From 223f268fccbc7de5a03eb64e496605f41d4a8e8c Mon Sep 17 00:00:00 2001 From: stslex Date: Wed, 2 Jul 2025 20:37:41 +0300 Subject: [PATCH 1/6] add settings feature --- commonApp/build.gradle.kts | 1 + .../kotlin/com/stslex/atten/di/AppModules.kt | 2 + .../stslex/atten/host/AppNavigationHost.kt | 2 + .../stslex/atten/host/DefaultRootComponent.kt | 2 + .../com/stslex/atten/host/RootComponent.kt | 3 + .../stslex/atten/core/ui/kit/mvi/Router.kt | 5 - .../com/stslex/atten/core/ui/kit/mvi/Store.kt | 127 ---------------- .../atten/core/ui/kit/mvi/StoreAbstraction.kt | 24 ---- .../atten/core/ui/kit/mvi/StoreComponent.kt | 64 --------- .../com/stslex/atten/core/ui/mvi/Feature.kt | 28 ++-- .../com/stslex/atten/core/ui/mvi/Router.kt | 6 - .../stslex/atten/core/ui/navigation/Config.kt | 3 + .../di/DetailsFeature.kt | 17 +-- .../di/HomeFeature.kt | 17 +-- .../ui/HomeScreen.kt | 3 + .../ui/HomeWidget.kt | 135 +++++++++++------- .../ui/mvi/HomeStore.kt | 6 + .../ui/mvi/handlers/ClickHandler.kt | 7 +- .../ui/mvi/handlers/HomeComponentImpl.kt | 1 + feature/settings/build.gradle.kts | 12 ++ .../settings/di/ModuleFeatureSettings.kt | 8 ++ .../feature/settings/di/SettingsFeature.kt | 32 +++++ .../feature/settings/di/SettingsScope.kt | 6 + .../feature/settings/mvi/SettingsComponent.kt | 15 ++ .../settings/mvi/SettingsHandlerStore.kt | 8 ++ .../feature/settings/mvi/SettingsStore.kt | 56 ++++++++ .../feature/settings/mvi/SettingsStoreImpl.kt | 39 +++++ .../settings/mvi/handlers/ClickHandler.kt | 30 ++++ .../settings/mvi/handlers/LifecycleHandler.kt | 44 ++++++ .../mvi/handlers/SettingsComponentImpl.kt | 18 +++ .../feature/settings/ui/SettingsScreen.kt | 18 +++ .../feature/settings/ui/SettingsWidget.kt | 53 +++++++ .../settings/ui/components/SettingsTopbar.kt | 39 +++++ settings.gradle.kts | 4 +- 34 files changed, 518 insertions(+), 317 deletions(-) delete mode 100644 core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/mvi/Router.kt delete mode 100644 core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/mvi/Store.kt delete mode 100644 core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/mvi/StoreAbstraction.kt delete mode 100644 core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/mvi/StoreComponent.kt delete mode 100644 core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/Router.kt create mode 100644 feature/settings/build.gradle.kts create mode 100644 feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/di/ModuleFeatureSettings.kt create mode 100644 feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/di/SettingsFeature.kt create mode 100644 feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/di/SettingsScope.kt create mode 100644 feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/SettingsComponent.kt create mode 100644 feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/SettingsHandlerStore.kt create mode 100644 feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/SettingsStore.kt create mode 100644 feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/SettingsStoreImpl.kt create mode 100644 feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/handlers/ClickHandler.kt create mode 100644 feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/handlers/LifecycleHandler.kt create mode 100644 feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/handlers/SettingsComponentImpl.kt create mode 100644 feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/ui/SettingsScreen.kt create mode 100644 feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/ui/SettingsWidget.kt create mode 100644 feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/ui/components/SettingsTopbar.kt diff --git a/commonApp/build.gradle.kts b/commonApp/build.gradle.kts index 67423a3..aae9eb7 100644 --- a/commonApp/build.gradle.kts +++ b/commonApp/build.gradle.kts @@ -16,6 +16,7 @@ kotlin { implementation(project(":feature:home")) implementation(project(":feature:details")) + implementation(project(":feature:settings")) } } } 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 90259f3..779530c 100644 --- a/commonApp/src/commonMain/kotlin/com/stslex/atten/di/AppModules.kt +++ b/commonApp/src/commonMain/kotlin/com/stslex/atten/di/AppModules.kt @@ -1,5 +1,6 @@ package com.stslex.atten.di +import com.stslex.atten.feature.settings.di.ModuleFeatureSettings import com.stslex.atten.core.core.di.ModuleCore import com.stslex.atten.core.database.di.ModuleCoreDatabase import com.stslex.atten.core.paging.di.ModuleCorePaging @@ -16,4 +17,5 @@ val appModules: List = listOf( ModuleCorePaging().module, ModuleFeatureHome().module, ModuleFeatureDetails().module, + ModuleFeatureSettings().module ) diff --git a/commonApp/src/commonMain/kotlin/com/stslex/atten/host/AppNavigationHost.kt b/commonApp/src/commonMain/kotlin/com/stslex/atten/host/AppNavigationHost.kt index cb6ff8c..4befaf1 100644 --- a/commonApp/src/commonMain/kotlin/com/stslex/atten/host/AppNavigationHost.kt +++ b/commonApp/src/commonMain/kotlin/com/stslex/atten/host/AppNavigationHost.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.arkivanov.decompose.extensions.compose.stack.Children import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation +import com.stslex.atten.feature.settings.ui.SettingsScreen import com.stslex.atten.feature.details.ui.DetailsScreen import com.stslex.atten.feature.home.ui.HomeScreen @@ -21,6 +22,7 @@ internal fun AppNavigationHost( when (val instance = created.instance) { is RootComponent.Child.Details -> DetailsScreen(instance.component) is RootComponent.Child.Home -> HomeScreen(instance.component) + is RootComponent.Child.Settings -> SettingsScreen(instance.component) } } } \ No newline at end of file diff --git a/commonApp/src/commonMain/kotlin/com/stslex/atten/host/DefaultRootComponent.kt b/commonApp/src/commonMain/kotlin/com/stslex/atten/host/DefaultRootComponent.kt index e6955bf..b1bddb1 100644 --- a/commonApp/src/commonMain/kotlin/com/stslex/atten/host/DefaultRootComponent.kt +++ b/commonApp/src/commonMain/kotlin/com/stslex/atten/host/DefaultRootComponent.kt @@ -8,6 +8,7 @@ import com.arkivanov.decompose.router.stack.childStack import com.arkivanov.decompose.router.stack.navigate import com.arkivanov.decompose.router.stack.pop import com.arkivanov.decompose.value.Value +import com.stslex.atten.feature.settings.mvi.SettingsComponent import com.stslex.atten.core.ui.navigation.Config import com.stslex.atten.core.ui.navigation.Router import com.stslex.atten.feature.details.ui.mvi.DetailsComponent @@ -40,6 +41,7 @@ class DefaultRootComponent( ): Child = when (config) { is Config.Home -> Child.Home(HomeComponent.create(context.router)) is Config.Detail -> Child.Details(DetailsComponent.create(context.router, config.uuid)) + is Config.Settings -> Child.Settings(SettingsComponent.create(context.router)) } @OptIn(DelicateDecomposeApi::class) diff --git a/commonApp/src/commonMain/kotlin/com/stslex/atten/host/RootComponent.kt b/commonApp/src/commonMain/kotlin/com/stslex/atten/host/RootComponent.kt index 72027d9..3bb9c9e 100644 --- a/commonApp/src/commonMain/kotlin/com/stslex/atten/host/RootComponent.kt +++ b/commonApp/src/commonMain/kotlin/com/stslex/atten/host/RootComponent.kt @@ -3,6 +3,7 @@ package com.stslex.atten.host import com.arkivanov.decompose.Cancellation import com.arkivanov.decompose.router.stack.ChildStack import com.arkivanov.decompose.value.Value +import com.stslex.atten.feature.settings.mvi.SettingsComponent import com.stslex.atten.core.ui.navigation.Config import com.stslex.atten.feature.details.ui.mvi.DetailsComponent import com.stslex.atten.feature.home.ui.mvi.HomeComponent @@ -18,6 +19,8 @@ interface RootComponent { data class Home(val component: HomeComponent) : Child data class Details(val component: DetailsComponent) : Child + + data class Settings(val component: SettingsComponent) : Child } } diff --git a/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/mvi/Router.kt b/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/mvi/Router.kt deleted file mode 100644 index 3bd46b9..0000000 --- a/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/mvi/Router.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.stslex.atten.core.ui.kit.mvi - -fun interface Router { - operator fun invoke(event: E) -} diff --git a/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/mvi/Store.kt b/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/mvi/Store.kt deleted file mode 100644 index 3da4639..0000000 --- a/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/mvi/Store.kt +++ /dev/null @@ -1,127 +0,0 @@ -package com.stslex.atten.core.ui.kit.mvi - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.stslex.atten.core.core.coroutine.dispatcher.AppDispatcher -import com.stslex.atten.core.core.coroutine.scope.AppCoroutineScope -import com.stslex.atten.core.core.logger.Log -import com.stslex.atten.core.ui.kit.mvi.StoreComponent.Action -import com.stslex.atten.core.ui.kit.mvi.StoreComponent.Event -import com.stslex.atten.core.ui.kit.mvi.StoreComponent.Navigation -import com.stslex.atten.core.ui.kit.mvi.StoreComponent.State -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update - -abstract class Store( - private val router: Router, - appDispatcher: AppDispatcher, - initialState: S -) : ViewModel(), StoreAbstraction { - - private val screenName = this::class.simpleName - - private val _event: MutableSharedFlow = MutableSharedFlow() - override val event: SharedFlow = _event.asSharedFlow() - - private val _state: MutableStateFlow = MutableStateFlow(initialState) - override val state: StateFlow = _state.asStateFlow() - - protected val scope: AppCoroutineScope = AppCoroutineScope( - scope = viewModelScope, - appDispatcher = appDispatcher - ) - - private var _lastAction: A? = null - protected val lastAction: A? - get() = _lastAction - - override fun dispatch(action: A) { - if (lastAction != action && action !is Action.RepeatLastAction) { - _lastAction = action - } - Log.d("$screenName dispatchAction: $action", TAG) - process(action) - } - - /** Process the action. This method should be overridden in the child class.*/ - protected abstract fun process(action: A) - - /** - * Updates the state of the screen. - * @param update - function that updates the state - * */ - protected fun updateState(update: (S) -> S) { - _state.update(update) - } - - /** - * Sends an event to the screen. The event is sent on the default dispatcher of the AppDispatcher. - * @param event - event to be sent - * @see AppDispatcher - * */ - protected fun sendEvent(event: E) { - Log.d("$screenName sendEvent: $event", TAG) - scope.launch { - this@Store._event.emit(event) - } - } - - /** - * Navigates to the specified screen. The router is called with the specified event. - * @param event - event to be passed to the router - * @see Router - * */ - protected fun consumeNavigation(event: N) { - Log.d("$screenName consumeNavigation: $event", TAG) - router(event) - } - - /** - * Launches a coroutine and catches exceptions. The coroutine is launched on the default dispatcher of the AppDispatcher. - * @param onError - error handler - * @param onSuccess - success handler - * @param action - action to be executed - * @return Job - * @see Job - * @see AppDispatcher - * */ - protected fun launch( - onError: suspend (Throwable) -> Unit = {}, - onSuccess: suspend CoroutineScope.(T) -> Unit = {}, - action: suspend CoroutineScope.() -> T, - ): Job = scope.launch( - onError = onError, - onSuccess = onSuccess, - action = action - ) - - /** - * Launches a flow and collects it in the screenModelScope. The flow is collected on the default dispatcher. of the AppDispatcher. - * @param onError - error handler - * @param each - action for each element of the flow - * @return Job - * @see Flow - * @see Job - * @see AppDispatcher - * */ - protected fun Flow.launch( - onError: suspend (cause: Throwable) -> Unit = {}, - each: suspend (T) -> Unit - ): Job = scope.launch( - flow = this, - onError = onError, - each = each, - ) - - companion object { - private const val TAG = "Store" - } -} \ No newline at end of file diff --git a/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/mvi/StoreAbstraction.kt b/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/mvi/StoreAbstraction.kt deleted file mode 100644 index ad547cd..0000000 --- a/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/mvi/StoreAbstraction.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.stslex.atten.core.ui.kit.mvi - -import com.stslex.atten.core.ui.kit.mvi.StoreComponent.Action -import com.stslex.atten.core.ui.kit.mvi.StoreComponent.Event -import com.stslex.atten.core.ui.kit.mvi.StoreComponent.State -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow - -interface StoreAbstraction { - - /** Flow of the state of the screen. */ - val state: StateFlow - - /** Flow of events that are sent to the screen. */ - val event: SharedFlow - - /** - * Sends an action to the store. Checks if the action is not the same as the last action. - * If the action is not the same as the last action, the last action is updated. - * The action is then processed. - * @param action - action to be sent - */ - fun dispatch(action: A) -} \ No newline at end of file diff --git a/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/mvi/StoreComponent.kt b/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/mvi/StoreComponent.kt deleted file mode 100644 index 9ae6379..0000000 --- a/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/mvi/StoreComponent.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.stslex.atten.core.ui.kit.mvi - -import androidx.compose.material3.SnackbarDuration -import androidx.compose.runtime.Stable -import com.stslex.atten.core.ui.kit.components.snackbar.SnackbarType - -interface StoreComponent { - - interface State : StoreComponent - - interface Navigation : StoreComponent - - interface Event : StoreComponent { - - @Stable - sealed class Snackbar( - open val message: String, - open val duration: SnackbarDuration, - open val withDismissAction: Boolean, - val action: String, - ) : Event { - - @Stable - data class Error( - override val message: String, - override val duration: SnackbarDuration = SnackbarDuration.Short, - override val withDismissAction: Boolean = false, - ) : Snackbar( - message = message, - action = SnackbarType.ERROR.label, - duration = duration, - withDismissAction = withDismissAction - ) - - @Stable - data class Success( - override val message: String, - override val duration: SnackbarDuration = SnackbarDuration.Short, - override val withDismissAction: Boolean = false, - ) : Snackbar( - message = message, - action = SnackbarType.SUCCESS.label, - duration = duration, - withDismissAction = withDismissAction - ) - - @Stable - data class Info( - override val message: String, - override val duration: SnackbarDuration = SnackbarDuration.Short, - override val withDismissAction: Boolean = false, - ) : Snackbar( - message = message, - action = SnackbarType.INFO.label, - duration = duration, - withDismissAction = withDismissAction - ) - } - } - - interface Action : StoreComponent { - interface RepeatLastAction : Action - } -} \ No newline at end of file diff --git a/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/Feature.kt b/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/Feature.kt index 88ca4cb..1975980 100644 --- a/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/Feature.kt +++ b/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/Feature.kt @@ -5,6 +5,9 @@ import com.stslex.atten.core.ui.mvi.processor.StoreProcessor import com.stslex.atten.core.ui.navigation.Component import org.koin.core.component.KoinScopeComponent import org.koin.core.module.Module +import org.koin.core.qualifier.qualifier +import org.koin.core.scope.Scope +import kotlin.reflect.KClass /** * Feature is a Koin feature module that provides a StoreProcessor. @@ -12,17 +15,26 @@ import org.koin.core.module.Module * * @see [StoreProcessor] * */ -interface Feature, TComponent : Component> : - KoinScopeComponent { +abstract class Feature, TComponent : Component>( + scopeClass: KClass<*> +) : KoinScopeComponent { - val loadModule: Boolean - get() = false + abstract val module: Module + + private val scopeName: String = requireNotNull(scopeClass.qualifiedName) { + "Scope name is null. Please check the SettingsFeature class." + } - val bindWithLifecycle: Boolean - get() = true + open val loadModule: Boolean + get() = false - val module: Module + final override val scope: Scope by lazy { + getKoin().getOrCreateScope( + scopeId = scopeName, + qualifier = qualifier(scopeName) + ) + } @Composable - fun processor(component: TComponent): TProcessor + abstract fun processor(component: TComponent): TProcessor } \ No newline at end of file diff --git a/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/Router.kt b/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/Router.kt deleted file mode 100644 index b9a3468..0000000 --- a/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/Router.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.stslex.atten.core.ui.mvi - -fun interface Router { - - operator fun invoke(event: E) -} diff --git a/core/ui/navigation/src/commonMain/kotlin/com/stslex/atten/core/ui/navigation/Config.kt b/core/ui/navigation/src/commonMain/kotlin/com/stslex/atten/core/ui/navigation/Config.kt index a8bd5e6..9cae8e5 100644 --- a/core/ui/navigation/src/commonMain/kotlin/com/stslex/atten/core/ui/navigation/Config.kt +++ b/core/ui/navigation/src/commonMain/kotlin/com/stslex/atten/core/ui/navigation/Config.kt @@ -20,4 +20,7 @@ sealed interface Config { data class Detail( val uuid: String ) : Config + + @Serializable + data object Settings : Config } \ No newline at end of file diff --git a/feature/details/src/commonMain/kotlin/com.stslex.atten.feature.details/di/DetailsFeature.kt b/feature/details/src/commonMain/kotlin/com.stslex.atten.feature.details/di/DetailsFeature.kt index 7094425..93615cf 100644 --- a/feature/details/src/commonMain/kotlin/com.stslex.atten.feature.details/di/DetailsFeature.kt +++ b/feature/details/src/commonMain/kotlin/com.stslex.atten.feature.details/di/DetailsFeature.kt @@ -9,8 +9,6 @@ import com.stslex.atten.feature.details.ui.mvi.DetailsStore.Action import com.stslex.atten.feature.details.ui.mvi.DetailsStore.Event import com.stslex.atten.feature.details.ui.mvi.DetailsStore.State import org.koin.core.module.Module -import org.koin.core.qualifier.qualifier -import org.koin.core.scope.Scope import org.koin.ksp.generated.module internal typealias DetailsStoreProcessor = StoreProcessor @@ -21,21 +19,12 @@ internal typealias DetailsStoreProcessor = StoreProcessor * * @see [com.stslex.atten.feature.details.ui.mvi.DetailsStore] * */ -internal object DetailsFeature : Feature { +internal object DetailsFeature : Feature( + scopeClass = DetailsScope::class +) { override val module: Module by lazy { ModuleFeatureDetails().module } - private val scopeName = requireNotNull(DetailsScope::class.qualifiedName) { - "Scope name is null. Please check the DetailsFeature class." - } - - override val scope: Scope by lazy { - getKoin().getOrCreateScope( - scopeId = scopeName, - qualifier = qualifier(scopeName) - ) - } - @Composable override fun processor( component: DetailsComponent diff --git a/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/di/HomeFeature.kt b/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/di/HomeFeature.kt index 1c4768b..a42db6a 100644 --- a/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/di/HomeFeature.kt +++ b/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/di/HomeFeature.kt @@ -9,8 +9,6 @@ import com.stslex.atten.feature.home.ui.mvi.HomeStore.Action import com.stslex.atten.feature.home.ui.mvi.HomeStore.Event import com.stslex.atten.feature.home.ui.mvi.HomeStore.State import org.koin.core.module.Module -import org.koin.core.qualifier.qualifier -import org.koin.core.scope.Scope import org.koin.ksp.generated.module internal typealias HomeStoreProcessor = StoreProcessor @@ -21,21 +19,12 @@ internal typealias HomeStoreProcessor = StoreProcessor * * @see [com.stslex.atten.feature.home.ui.mvi.HomeStore] * */ -internal object HomeFeature : Feature { +internal object HomeFeature : Feature( + scopeClass = HomeScope::class +) { override val module: Module by lazy { ModuleFeatureHome().module } - private val scopeName = requireNotNull(HomeScope::class.qualifiedName) { - "Scope name is null. Please check the HomeFeature class." - } - - override val scope: Scope by lazy { - getKoin().getOrCreateScope( - scopeId = scopeName, - qualifier = qualifier(scopeName) - ) - } - @Composable override fun processor( component: HomeComponent diff --git a/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/HomeScreen.kt b/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/HomeScreen.kt index b214bf2..8482fe4 100644 --- a/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/HomeScreen.kt +++ b/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/HomeScreen.kt @@ -56,6 +56,9 @@ fun HomeScreen(component: HomeComponent) { }, onItemLongCLick = { id -> processor.consume(Action.Click.OnSelectItemClicked(id)) + }, + onSettingsClick = { + processor.consume(Action.Click.OnSettingsClicked) } ) } diff --git a/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/HomeWidget.kt b/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/HomeWidget.kt index f6c2418..60e8bcb 100644 --- a/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/HomeWidget.kt +++ b/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/HomeWidget.kt @@ -9,7 +9,6 @@ import androidx.compose.animation.scaleOut import androidx.compose.animation.togetherWith import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -21,13 +20,19 @@ import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Login import androidx.compose.material.icons.filled.Create import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FabPosition import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.stslex.atten.core.paging.model.PagingConfig import com.stslex.atten.core.paging.states.PagingUiState @@ -43,7 +48,7 @@ import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.toImmutableList import org.jetbrains.compose.ui.tooling.preview.Preview -@OptIn(ExperimentalFoundationApi::class) +@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) @Composable internal fun HomeWidget( state: State, @@ -53,16 +58,86 @@ internal fun HomeWidget( onLoadNext: () -> Unit, onCreateItemClick: () -> Unit, onDeleteItemsClick: () -> Unit, + onSettingsClick: () -> Unit, modifier: Modifier = Modifier, ) { - Box( + Scaffold( modifier = modifier .fillMaxSize() .background(MaterialTheme.colorScheme.background) .systemBarsPadding(), - ) { + topBar = { + TopAppBar( + title = { + Text( + text = "Home", + style = MaterialTheme.typography.titleLarge + ) + }, + navigationIcon = { + IconButton( + onClick = onSettingsClick, + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.Login, + contentDescription = "Back", + tint = MaterialTheme.colorScheme.onBackground, + ) + } + }, + ) + }, + floatingActionButtonPosition = FabPosition.EndOverlay, + floatingActionButton = { + val buttonShapeRadius by animateDpAsState( + targetValue = if (state.selectedItems.isNotEmpty()) { + AppDimension.Radius.largest + } else { + AppDimension.Radius.medium + } + ) + CardWithAnimatedBorder( + modifier = Modifier + .padding(AppDimension.Padding.big) + .wrapContentSize() + .width(IntrinsicSize.Max) + .height(IntrinsicSize.Max), + onClick = { + if (state.selectedItems.isNotEmpty()) { + onDeleteItemsClick() + } else { + onCreateItemClick() + } + }, + isAnimated = state.selectedItems.isNotEmpty(), + cornerRadius = buttonShapeRadius, + backgroundColor = MaterialTheme.colorScheme.secondaryContainer, + disableBorderColor = MaterialTheme.colorScheme.onSecondaryContainer + ) { + AnimatedContent( + modifier = Modifier.padding(AppDimension.Padding.big), + targetState = state.selectedItems.isNotEmpty(), + transitionSpec = { + fadeIn().plus(scaleIn()) togetherWith + fadeOut().plus(scaleOut()) + } + ) { isDeleting -> + Icon( + imageVector = if (isDeleting) { + Icons.Filled.Delete + } else { + Icons.Filled.Create + }, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSecondaryContainer + ) + } + } + } + ) { paddingValues -> PagingColumn( modifier = Modifier + .padding(paddingValues) .fillMaxSize() .padding( horizontal = AppDimension.Padding.medium, @@ -86,53 +161,6 @@ internal fun HomeWidget( Spacer(modifier = Modifier.height(AppDimension.Padding.medium)) } } - - val buttonShapeRadius by animateDpAsState( - targetValue = if (state.selectedItems.isNotEmpty()) { - AppDimension.Radius.largest - } else { - AppDimension.Radius.medium - } - ) - - CardWithAnimatedBorder( - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(AppDimension.Padding.big) - .wrapContentSize() - .width(IntrinsicSize.Max) - .height(IntrinsicSize.Max), - onClick = { - if (state.selectedItems.isNotEmpty()) { - onDeleteItemsClick() - } else { - onCreateItemClick() - } - }, - isAnimated = state.selectedItems.isNotEmpty(), - cornerRadius = buttonShapeRadius, - backgroundColor = MaterialTheme.colorScheme.secondaryContainer, - disableBorderColor = MaterialTheme.colorScheme.onSecondaryContainer - ) { - AnimatedContent( - modifier = Modifier.padding(AppDimension.Padding.big), - targetState = state.selectedItems.isNotEmpty(), - transitionSpec = { - fadeIn().plus(scaleIn()) togetherWith - fadeOut().plus(scaleOut()) - } - ) { isDeleting -> - Icon( - imageVector = if (isDeleting) { - Icons.Filled.Delete - } else { - Icons.Filled.Create - }, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSecondaryContainer - ) - } - } } } @@ -163,7 +191,8 @@ internal fun HomeScreenPreview() { onItemClicked = {}, onCreateItemClick = {}, onItemLongCLick = {}, - onDeleteItemsClick = {} + onDeleteItemsClick = {}, + onSettingsClick = {} ) } } \ No newline at end of file diff --git a/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/mvi/HomeStore.kt b/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/mvi/HomeStore.kt index fef8c81..3a891a4 100644 --- a/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/mvi/HomeStore.kt +++ b/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/mvi/HomeStore.kt @@ -102,6 +102,9 @@ interface HomeStore : Store { @Stable data object OnCreateItemClicked : Click + + @Stable + data object OnSettingsClicked : Click } @Stable @@ -110,6 +113,9 @@ interface HomeStore : Store { @Stable data class NavigateToDetail(val id: String) : Navigation + @Stable + data object Settings : Navigation + @Stable data object Back : Navigation } diff --git a/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/mvi/handlers/ClickHandler.kt b/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/mvi/handlers/ClickHandler.kt index 8a6e016..3bf8974 100644 --- a/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/mvi/handlers/ClickHandler.kt +++ b/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/mvi/handlers/ClickHandler.kt @@ -1,12 +1,12 @@ package com.stslex.atten.feature.home.ui.mvi.handlers +import com.stslex.atten.core.ui.mvi.handler.Handler import com.stslex.atten.feature.home.di.HomeScope import com.stslex.atten.feature.home.domain.interactor.HomeScreenInteractor import com.stslex.atten.feature.home.domain.model.CreateTodoDomainModel import com.stslex.atten.feature.home.ui.mvi.HomeHandlerStore import com.stslex.atten.feature.home.ui.mvi.HomeStore.Action import com.stslex.atten.feature.home.ui.mvi.HomeStore.Event -import com.stslex.atten.core.ui.mvi.handler.Handler import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.toImmutableSet import org.koin.core.annotation.Factory @@ -27,9 +27,14 @@ internal class ClickHandler( is Action.Click.OnDeleteItemsClicked -> actionOnDeleteItemClicked() is Action.Click.OnItemClicked -> actionOnItemClicked(action) is Action.Click.OnSelectItemClicked -> actionOnSelectItemClicked(action) + is Action.Click.OnSettingsClicked -> actionSettingsClick() } } + private fun HomeHandlerStore.actionSettingsClick() { + consume(Action.Navigation.Settings) + } + private fun HomeHandlerStore.actionOnItemClicked( action: Action.Click.OnItemClicked ) { diff --git a/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/mvi/handlers/HomeComponentImpl.kt b/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/mvi/handlers/HomeComponentImpl.kt index a361dc5..cb33d4f 100644 --- a/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/mvi/handlers/HomeComponentImpl.kt +++ b/feature/home/src/commonMain/kotlin/com.stslex.atten.feature.home/ui/mvi/handlers/HomeComponentImpl.kt @@ -15,6 +15,7 @@ internal class HomeComponentImpl( when (action) { is Action.Navigation.Back -> router.popBack() is Action.Navigation.NavigateToDetail -> router.navTo(Config.Detail(action.id)) + is Action.Navigation.Settings -> router.navTo(Config.Settings) } } } \ No newline at end of file diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts new file mode 100644 index 0000000..e662b7b --- /dev/null +++ b/feature/settings/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + alias(libs.plugins.convention.kmp.feature) +} + +kotlin { + sourceSets.commonMain.dependencies { + implementation(project(":core:core")) + implementation(project(":core:ui:kit")) + implementation(project(":core:ui:mvi")) + implementation(project(":core:ui:navigation")) + } +} diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/di/ModuleFeatureSettings.kt b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/di/ModuleFeatureSettings.kt new file mode 100644 index 0000000..ed6eb84 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/di/ModuleFeatureSettings.kt @@ -0,0 +1,8 @@ +package com.stslex.atten.feature.settings.di + +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module + +@Module +@ComponentScan("com.stslex.atten.feature.settings") +class ModuleFeatureSettings diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/di/SettingsFeature.kt b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/di/SettingsFeature.kt new file mode 100644 index 0000000..6d3e6fc --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/di/SettingsFeature.kt @@ -0,0 +1,32 @@ +package com.stslex.atten.feature.settings.di + +import androidx.compose.runtime.Composable +import com.stslex.atten.feature.settings.mvi.SettingsComponent +import com.stslex.atten.feature.settings.mvi.SettingsStore.Action +import com.stslex.atten.feature.settings.mvi.SettingsStore.Event +import com.stslex.atten.feature.settings.mvi.SettingsStore.State +import com.stslex.atten.core.ui.mvi.Feature +import com.stslex.atten.core.ui.mvi.processor.StoreProcessor +import com.stslex.atten.core.ui.mvi.processor.rememberStoreProcessor +import org.koin.core.module.Module +import org.koin.ksp.generated.module + +internal typealias SettingsStoreProcessor = StoreProcessor + +/** + * SettingsFeature is a Koin feature module that provides the SettingsStore processor. + * It is responsible for managing the state and actions related to the profile feature. + * + * @see [com.stslex.atten.feature.settings.mvi.SettingsStore] + * */ +internal object SettingsFeature : Feature( + scopeClass = SettingsScope::class +) { + + override val module: Module by lazy { ModuleFeatureSettings().module } + + @Composable + override fun processor( + component: SettingsComponent + ): SettingsStoreProcessor = rememberStoreProcessor(component) +} diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/di/SettingsScope.kt b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/di/SettingsScope.kt new file mode 100644 index 0000000..2b27a72 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/di/SettingsScope.kt @@ -0,0 +1,6 @@ +package com.stslex.atten.feature.settings.di + +import org.koin.core.annotation.Scope + +@Scope(SettingsScope::class) +class SettingsScope \ No newline at end of file diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/SettingsComponent.kt b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/SettingsComponent.kt new file mode 100644 index 0000000..c3869a7 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/SettingsComponent.kt @@ -0,0 +1,15 @@ +package com.stslex.atten.feature.settings.mvi + +import com.stslex.atten.feature.settings.mvi.SettingsStore.Action +import com.stslex.atten.feature.settings.mvi.handlers.SettingsComponentImpl +import com.stslex.atten.core.ui.mvi.handler.Handler +import com.stslex.atten.core.ui.navigation.Component +import com.stslex.atten.core.ui.navigation.Router + +interface SettingsComponent : Component, Handler { + + companion object { + + fun create(router: Router): SettingsComponent = SettingsComponentImpl(router) + } +} \ No newline at end of file diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/SettingsHandlerStore.kt b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/SettingsHandlerStore.kt new file mode 100644 index 0000000..8fc000c --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/SettingsHandlerStore.kt @@ -0,0 +1,8 @@ +package com.stslex.atten.feature.settings.mvi + +import com.stslex.atten.feature.settings.mvi.SettingsStore.Action +import com.stslex.atten.feature.settings.mvi.SettingsStore.Event +import com.stslex.atten.feature.settings.mvi.SettingsStore.State +import com.stslex.atten.core.ui.mvi.handler.HandlerStore + +interface SettingsHandlerStore : HandlerStore, SettingsStore \ No newline at end of file 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 new file mode 100644 index 0000000..6316982 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/SettingsStore.kt @@ -0,0 +1,56 @@ +package com.stslex.atten.feature.settings.mvi + +import androidx.compose.runtime.Stable +import com.stslex.atten.feature.settings.mvi.SettingsStore.Action +import com.stslex.atten.feature.settings.mvi.SettingsStore.Event +import com.stslex.atten.feature.settings.mvi.SettingsStore.State +import com.stslex.atten.core.ui.mvi.Store + +interface SettingsStore : Store { + + @Stable + data class State( + val username: String = "", + ) : Store.State { + + companion object { + + val INIT = State() + } + } + + @Stable + sealed interface Action : Store.Action { + + @Stable + sealed interface Lifecycle : Action { + + @Stable + data object Init : Lifecycle + + @Stable + data object Dispose : Lifecycle + } + + @Stable + sealed interface Click : Action { + + @Stable + data object Back : Click + + @Stable + data object Login : Click + } + + @Stable + sealed interface Navigation : Action { + + @Stable + data object NavBack : Navigation + } + } + + @Stable + sealed interface Event : Store.Event +} + diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/SettingsStoreImpl.kt b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/SettingsStoreImpl.kt new file mode 100644 index 0000000..5199969 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/SettingsStoreImpl.kt @@ -0,0 +1,39 @@ +package com.stslex.atten.feature.settings.mvi + +import com.stslex.atten.feature.settings.di.SettingsScope +import com.stslex.atten.feature.settings.mvi.SettingsStore.Action +import com.stslex.atten.feature.settings.mvi.SettingsStore.Event +import com.stslex.atten.feature.settings.mvi.SettingsStore.State +import com.stslex.atten.feature.settings.mvi.handlers.ClickHandler +import com.stslex.atten.feature.settings.mvi.handlers.LifecycleHandler +import com.stslex.atten.core.core.coroutine.dispatcher.AppDispatcher +import com.stslex.atten.core.ui.mvi.BaseStore +import org.koin.android.annotation.KoinViewModel +import org.koin.core.annotation.InjectedParam +import org.koin.core.annotation.Qualifier +import org.koin.core.annotation.Scope +import org.koin.core.annotation.Scoped + +@KoinViewModel +@Scope(SettingsScope::class) +@Scoped +@Qualifier(SettingsScope::class) +class SettingsStoreImpl( + @InjectedParam component: SettingsComponent, + appDispatcher: AppDispatcher, + clickHandler: ClickHandler, + lifecycleHandler: LifecycleHandler, +) : SettingsHandlerStore, BaseStore( + name = "SETTINGS", + initialState = State.INIT, + appDispatcher = appDispatcher, + handlerCreator = { action -> + when (action) { + is Action.Click -> clickHandler + is Action.Lifecycle -> lifecycleHandler + is Action.Navigation -> component + } + }, + disposeActions = listOf(Action.Lifecycle.Dispose), + initialActions = listOf(Action.Lifecycle.Init), +) \ 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 new file mode 100644 index 0000000..f18af20 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/handlers/ClickHandler.kt @@ -0,0 +1,30 @@ +package com.stslex.atten.feature.settings.mvi.handlers + +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 com.stslex.atten.core.ui.mvi.handler.Handler +import org.koin.core.annotation.Factory +import org.koin.core.annotation.Scope +import org.koin.core.annotation.Scoped + +@Factory +@Scope(SettingsScope::class) +@Scoped() +class ClickHandler : Handler { + + override fun SettingsHandlerStore.invoke(action: Action.Click) { + when (action) { + Action.Click.Back -> actionBack() + Action.Click.Login -> actionLogin() + } + } + + private fun SettingsHandlerStore.actionBack() { + consume(Action.Navigation.NavBack) + } + + private fun SettingsHandlerStore.actionLogin() { + // Handle login action + } +} \ No newline at end of file diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/handlers/LifecycleHandler.kt b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/handlers/LifecycleHandler.kt new file mode 100644 index 0000000..49e1062 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/handlers/LifecycleHandler.kt @@ -0,0 +1,44 @@ +package com.stslex.atten.feature.settings.mvi.handlers + +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 com.stslex.atten.core.ui.mvi.handler.Handler +import kotlinx.coroutines.delay +import org.koin.core.annotation.Factory +import org.koin.core.annotation.Scope +import org.koin.core.annotation.Scoped + +@Factory +@Scope(SettingsScope::class) +@Scoped() +class LifecycleHandler : Handler { + + override fun SettingsHandlerStore.invoke(action: Action.Lifecycle) { + when (action) { + Action.Lifecycle.Init -> actionInit() + Action.Lifecycle.Dispose -> actionDispose() + } + } + + private fun SettingsHandlerStore.actionInit() { + // Handle initialization logic + updateState { currentState -> + currentState.copy( + username = "loading..." // Initial state before fetching username + ) + } + launch { + delay(10_000) + updateState { currentState -> + currentState.copy( + username = "User123" // Simulate fetching username + ) + } + } + } + + private fun SettingsHandlerStore.actionDispose() { + // Handle disposal logic + } +} \ No newline at end of file diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/handlers/SettingsComponentImpl.kt b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/handlers/SettingsComponentImpl.kt new file mode 100644 index 0000000..f3f4e5f --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/handlers/SettingsComponentImpl.kt @@ -0,0 +1,18 @@ +package com.stslex.atten.feature.settings.mvi.handlers + +import com.arkivanov.decompose.ComponentContext +import com.stslex.atten.feature.settings.mvi.SettingsComponent +import com.stslex.atten.feature.settings.mvi.SettingsHandlerStore +import com.stslex.atten.feature.settings.mvi.SettingsStore.Action.Navigation +import com.stslex.atten.core.ui.navigation.Router + +class SettingsComponentImpl( + private val router: Router +) : SettingsComponent, ComponentContext by router { + + override fun SettingsHandlerStore.invoke(action: Navigation) { + when (action) { + Navigation.NavBack -> router.popBack() + } + } +} \ No newline at end of file diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/ui/SettingsScreen.kt b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/ui/SettingsScreen.kt new file mode 100644 index 0000000..8daaec1 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/ui/SettingsScreen.kt @@ -0,0 +1,18 @@ +package com.stslex.atten.feature.settings.ui + +import androidx.compose.runtime.Composable +import com.stslex.atten.feature.settings.di.SettingsFeature +import com.stslex.atten.feature.settings.mvi.SettingsComponent +import com.stslex.atten.core.ui.mvi.NavComponentScreen + +@Composable +fun SettingsScreen( + component: SettingsComponent +) { + NavComponentScreen(SettingsFeature, component) { processor -> + SettingsWidget( + state = processor.state.value, + consume = processor::consume, + ) + } +} \ 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 new file mode 100644 index 0000000..7c0aeb2 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/ui/SettingsWidget.kt @@ -0,0 +1,53 @@ +package com.stslex.atten.feature.settings.ui + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +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( + state: State, + consume: (Action) -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier.fillMaxSize(), + topBar = { + SettingsTopbar( + title = state.username, + onBackClick = { consume(Action.Click.Back) }, + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + Text( + text = "Settings Screen", + style = MaterialTheme.typography.bodyLarge, + ) + + Button( + onClick = { consume(Action.Click.Login) }, + modifier = Modifier.padding(top = 16.dp) + ) { + Text(text = "Login") + } + + } + } +} \ No newline at end of file diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/ui/components/SettingsTopbar.kt b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/ui/components/SettingsTopbar.kt new file mode 100644 index 0000000..7fbed48 --- /dev/null +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/ui/components/SettingsTopbar.kt @@ -0,0 +1,39 @@ +package com.stslex.atten.feature.settings.ui.components + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun SettingsTopbar( + title: String, + onBackClick: () -> Unit, + modifier: Modifier = Modifier +) { + TopAppBar( + modifier = modifier, + title = { + Text( + text = title, + style = MaterialTheme.typography.titleLarge + ) + }, + navigationIcon = { + IconButton(onClick = onBackClick) { + Icon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowLeft, + contentDescription = "Back", + tint = MaterialTheme.colorScheme.onBackground, + ) + } + }, + ) +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index db57c59..fadb089 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -8,6 +8,7 @@ pluginManagement { } } +@Suppress("UnstableApiUsage") dependencyResolutionManagement { repositories { google() @@ -27,4 +28,5 @@ include(":core:paging") include(":core:todo") include(":feature:home") -include(":feature:details") \ No newline at end of file +include(":feature:details") +include(":feature:settings") \ No newline at end of file From 09a1ead6fd369ba7380d6e15848b2149936d4675 Mon Sep 17 00:00:00 2001 From: stslex Date: Wed, 2 Jul 2025 22:41:27 +0300 Subject: [PATCH 2/6] init google auth for android and signing --- .../kotlin/KMPApplicationConventionPlugin.kt | 60 +++++++++ commonApp/build.gradle.kts | 1 + core/auth/build.gradle.kts | 14 ++ .../core/auth/ui/GoogleAuthInit.android.kt | 126 ++++++++++++++++++ .../stslex/atten/core/auth/model/AuthEvent.kt | 13 ++ .../stslex/atten/core/auth/model/AuthState.kt | 23 ++++ .../atten/core/auth/model/GoogleAuthResult.kt | 9 ++ .../atten/core/auth/state/GoogleAuthState.kt | 18 +++ .../core/auth/state/GoogleAuthStateImpl.kt | 46 +++++++ .../atten/core/auth/ui/GoogleAuthInit.kt | 16 +++ .../atten/core/auth/ui/GoogleAuthInit.ios.kt | 8 ++ feature/settings/build.gradle.kts | 1 + .../feature/settings/mvi/SettingsStore.kt | 7 +- .../settings/mvi/handlers/ClickHandler.kt | 5 +- .../feature/settings/ui/SettingsScreen.kt | 24 +++- gradle/libs.versions.toml | 4 + settings.gradle.kts | 1 + 17 files changed, 371 insertions(+), 5 deletions(-) create mode 100644 core/auth/build.gradle.kts create mode 100644 core/auth/src/androidMain/kotlin/com/stslex/atten/core/auth/ui/GoogleAuthInit.android.kt create mode 100644 core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/model/AuthEvent.kt create mode 100644 core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/model/AuthState.kt create mode 100644 core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/model/GoogleAuthResult.kt create mode 100644 core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/state/GoogleAuthState.kt create mode 100644 core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/state/GoogleAuthStateImpl.kt create mode 100644 core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/ui/GoogleAuthInit.kt create mode 100644 core/auth/src/iosMain/kotlin/com/stslex/atten/core/auth/ui/GoogleAuthInit.ios.kt diff --git a/build-logic/convention/src/main/kotlin/KMPApplicationConventionPlugin.kt b/build-logic/convention/src/main/kotlin/KMPApplicationConventionPlugin.kt index 1de51fd..3c17f39 100644 --- a/build-logic/convention/src/main/kotlin/KMPApplicationConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/KMPApplicationConventionPlugin.kt @@ -14,6 +14,10 @@ import com.stslex.atten.convention.configureKsp import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.configure +import java.io.File +import java.io.FileInputStream +import java.io.InputStreamReader +import java.util.Properties class KMPApplicationConventionPlugin : Plugin { @@ -43,5 +47,61 @@ class KMPApplicationConventionPlugin : Plugin { versionCode = libs.findVersionInt("versionCode") } } + configureSigning() } +} + +fun Project.configureSigning() = extensions.configure { + signingConfigs { + val keystoreProperties = gradleKeystoreProperties(project.rootProject.projectDir) + create("release") { + keyAlias = keystoreProperties.getProperty("keyAlias") + keyPassword = keystoreProperties.getProperty("keyPassword") + storeFile = project.getFile(keystoreProperties.getProperty("storeFile")) + storePassword = keystoreProperties.getProperty("storePassword") + } + with(getByName("debug")) { + keyAlias = keystoreProperties.getProperty("keyAlias") + keyPassword = keystoreProperties.getProperty("keyPassword") + storeFile = project.getFile(keystoreProperties.getProperty("storeFile")) + storePassword = keystoreProperties.getProperty("storePassword") + } + } + buildTypes { + getByName("release") { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + signingConfig = signingConfigs.getByName("release") + isDebuggable = false + } + getByName("debug") { + signingConfig = signingConfigs.getByName("debug") + isDebuggable = true + } + } +} + + +fun Project.getFile(path: String): File { + val file = File(project.rootProject.projectDir, path) + if (file.isFile) { + return file + } else { + throw IllegalStateException("${file.name} is inValid") + } +} + +fun gradleKeystoreProperties(projectRootDir: File): Properties { + val properties = Properties() + val localProperties = File(projectRootDir, "keystore.properties") + + if (localProperties.isFile) { + InputStreamReader(FileInputStream(localProperties), Charsets.UTF_8).use { reader -> + properties.load(reader) + } + } + return properties } \ No newline at end of file diff --git a/commonApp/build.gradle.kts b/commonApp/build.gradle.kts index aae9eb7..cd36879 100644 --- a/commonApp/build.gradle.kts +++ b/commonApp/build.gradle.kts @@ -13,6 +13,7 @@ kotlin { implementation(project(":core:database")) implementation(project(":core:paging")) implementation(project(":core:todo")) + implementation(project(":core:auth")) implementation(project(":feature:home")) implementation(project(":feature:details")) diff --git a/core/auth/build.gradle.kts b/core/auth/build.gradle.kts new file mode 100644 index 0000000..456646a --- /dev/null +++ b/core/auth/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + alias(libs.plugins.convention.kmp.library.compose) +} + +kotlin { + sourceSets.apply { + commonMain.dependencies { + implementation(project(":core:core")) + } + androidMain.dependencies { + implementation(libs.gms.auth) + } + } +} \ No newline at end of file diff --git a/core/auth/src/androidMain/kotlin/com/stslex/atten/core/auth/ui/GoogleAuthInit.android.kt b/core/auth/src/androidMain/kotlin/com/stslex/atten/core/auth/ui/GoogleAuthInit.android.kt new file mode 100644 index 0000000..6b79355 --- /dev/null +++ b/core/auth/src/androidMain/kotlin/com/stslex/atten/core/auth/ui/GoogleAuthInit.android.kt @@ -0,0 +1,126 @@ +package com.stslex.atten.core.auth.ui + +import android.app.Activity +import android.content.IntentSender.SendIntentException +import androidx.activity.compose.LocalActivity +import androidx.activity.compose.ManagedActivityResultLauncher +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.ActivityResult +import androidx.activity.result.IntentSenderRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.coroutineScope +import com.google.android.gms.auth.api.identity.AuthorizationRequest +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.model.AuthEvent +import com.stslex.atten.core.auth.model.GoogleAuthResult +import com.stslex.atten.core.auth.state.GoogleAuthState +import com.stslex.atten.core.core.logger.Log +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + + +@Composable +internal actual fun GoogleAuthPlatformInit(state: GoogleAuthState) { + val activity = requireNotNull(LocalActivity.current) + val lifecycleOwner = LocalLifecycleOwner.current + + val launcher = rememberLauncher(state::consumeResult) + + DisposableEffect(lifecycleOwner) { + Log.tag("GOOGLE_AUTH").d("launch_auth") + val eventJob = state.event + .onEach { event -> + Log.tag("GOOGLE_AUTH").d("collect: $event") + when (event) { + AuthEvent.Auth -> processAuth( + activity = activity, + launcher = launcher, + processResult = state::consumeResult + ) + + AuthEvent.Cancel -> Unit + } + }.launchIn(lifecycleOwner.lifecycle.coroutineScope) + + onDispose { + Log.tag("GOOGLE_AUTH").d("dispose_auth") + eventJob.cancel() + } + } +} + +@Composable +private fun rememberLauncher( + processResult: (Result) -> Unit +): ManagedActivityResultLauncher = + checkNotNull(LocalActivity.current).let { activity -> + rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartIntentSenderForResult() + ) { result -> + Log.tag("GOOGLE_AUTH").d("activity_result: $result, ${result.data}") + result.data?.extras?.keySet()?.forEach { key -> + Log.tag("GOOGLE_AUTH").d("[$key] = ${result.data?.extras?.get(key)}") + } + 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)) + } + processResult(result) + } + } + + +// todo replace with handler in di (not ui object) +private fun processAuth( + activity: Activity, + launcher: ManagedActivityResultLauncher, + processResult: (Result) -> Unit +) { + val authRequest = AuthorizationRequest.Builder() + .setRequestedScopes(listOf(Scope(Scopes.EMAIL))) + .build() + Identity.getAuthorizationClient(activity) + .authorize(authRequest) + .addOnCanceledListener { + Log.tag("GOOGLE_AUTH").d("addOnCanceledListener") + } + .addOnFailureListener { e -> + Log.tag("GOOGLE_AUTH").e(e, "addOnFailureListener") + processResult(Result.failure(e)) + } + .addOnSuccessListener { result -> + Log.tag("GOOGLE_AUTH").d("on success: $result") + if (result.hasResolution()) { + try { + val pendingIntent = checkNotNull(result.pendingIntent) + val intentSenderRequest = IntentSenderRequest.Builder(pendingIntent).build() + launcher.launch(intentSenderRequest) + } catch (e: SendIntentException) { + Log.e( + e, + "Couldn't start Authorization UI: " + e.localizedMessage + ) + } + } else { + val uiResult = GoogleAuthResult( + accessToken = result.accessToken, + serverAuthCode = result.serverAuthCode + ) + processResult(Result.success(uiResult)) + } + } +} diff --git a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/model/AuthEvent.kt b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/model/AuthEvent.kt new file mode 100644 index 0000000..256a757 --- /dev/null +++ b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/model/AuthEvent.kt @@ -0,0 +1,13 @@ +package com.stslex.atten.core.auth.model + +import androidx.compose.runtime.Stable + +@Stable +sealed interface AuthEvent { + + @Stable + data object Auth : AuthEvent + + @Stable + data object Cancel : AuthEvent +} \ No newline at end of file diff --git a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/model/AuthState.kt b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/model/AuthState.kt new file mode 100644 index 0000000..ddcf6e7 --- /dev/null +++ b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/model/AuthState.kt @@ -0,0 +1,23 @@ +package com.stslex.atten.core.auth.model + +import androidx.compose.runtime.Stable + +@Stable +sealed interface AuthState { + + @Stable + data object Idle : AuthState + + @Stable + data object Loading : AuthState + + @Stable + data class Success( + val result: GoogleAuthResult + ) : AuthState + + @Stable + data class Error( + val error: Throwable + ) : AuthState +} \ No newline at end of file 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 new file mode 100644 index 0000000..d984133 --- /dev/null +++ b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/model/GoogleAuthResult.kt @@ -0,0 +1,9 @@ +package com.stslex.atten.core.auth.model + +import androidx.compose.runtime.Stable + +@Stable +data class GoogleAuthResult( + val serverAuthCode: String?, + val accessToken: String?, +) \ No newline at end of file diff --git a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/state/GoogleAuthState.kt b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/state/GoogleAuthState.kt new file mode 100644 index 0000000..6ac5518 --- /dev/null +++ b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/state/GoogleAuthState.kt @@ -0,0 +1,18 @@ +package com.stslex.atten.core.auth.state + +import com.stslex.atten.core.auth.model.AuthEvent +import com.stslex.atten.core.auth.model.AuthState +import com.stslex.atten.core.auth.model.GoogleAuthResult +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow + +interface GoogleAuthState { + + val state: StateFlow + + val event: SharedFlow + + suspend fun consume(event: AuthEvent) + + fun consumeResult(result: Result) +} \ No newline at end of file diff --git a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/state/GoogleAuthStateImpl.kt b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/state/GoogleAuthStateImpl.kt new file mode 100644 index 0000000..1234cf8 --- /dev/null +++ b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/state/GoogleAuthStateImpl.kt @@ -0,0 +1,46 @@ +package com.stslex.atten.core.auth.state + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.remember +import com.stslex.atten.core.auth.model.AuthEvent +import com.stslex.atten.core.auth.model.AuthState +import com.stslex.atten.core.auth.model.GoogleAuthResult +import com.stslex.atten.core.core.logger.Log +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow + +@Immutable +class GoogleAuthStateImpl private constructor() : GoogleAuthState { + + private val _state: MutableStateFlow = MutableStateFlow(AuthState.Idle) + override val state: StateFlow = _state.asStateFlow() + + private val _event: MutableSharedFlow = MutableSharedFlow() + override val event: SharedFlow = _event.asSharedFlow() + + override suspend fun consume(event: AuthEvent) { + logger.d("consume: $event") + _event.emit(event) + } + + override fun consumeResult(result: Result) { + logger.d("result: $result") + result + .onFailure { _state.value = AuthState.Error(it) } + .onSuccess { _state.value = AuthState.Success(it) } + } + + + companion object { + + private val logger = Log.tag("GoogleAuthState") + + @Composable + fun rememberGoogleAuthState(): GoogleAuthState = remember { GoogleAuthStateImpl() } + } +} \ No newline at end of file diff --git a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/ui/GoogleAuthInit.kt b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/ui/GoogleAuthInit.kt new file mode 100644 index 0000000..94247e2 --- /dev/null +++ b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/ui/GoogleAuthInit.kt @@ -0,0 +1,16 @@ +package com.stslex.atten.core.auth.ui + +import androidx.compose.runtime.Composable +import com.stslex.atten.core.auth.state.GoogleAuthState + +@Composable +fun GoogleAuthInit( + state: GoogleAuthState, +) { + GoogleAuthPlatformInit(state) +} + +@Composable +internal expect fun GoogleAuthPlatformInit( + state: GoogleAuthState, +) \ No newline at end of file diff --git a/core/auth/src/iosMain/kotlin/com/stslex/atten/core/auth/ui/GoogleAuthInit.ios.kt b/core/auth/src/iosMain/kotlin/com/stslex/atten/core/auth/ui/GoogleAuthInit.ios.kt new file mode 100644 index 0000000..63212e2 --- /dev/null +++ b/core/auth/src/iosMain/kotlin/com/stslex/atten/core/auth/ui/GoogleAuthInit.ios.kt @@ -0,0 +1,8 @@ +package com.stslex.atten.core.auth.ui + +import androidx.compose.runtime.Composable +import com.stslex.atten.core.auth.state.GoogleAuthState + +@Composable +internal actual fun GoogleAuthPlatformInit(state: GoogleAuthState) { +} \ No newline at end of file diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts index e662b7b..513ce26 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -8,5 +8,6 @@ kotlin { implementation(project(":core:ui:kit")) implementation(project(":core:ui:mvi")) implementation(project(":core:ui:navigation")) + implementation(project(":core:auth")) } } 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 6316982..ac5816b 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 @@ -1,10 +1,10 @@ package com.stslex.atten.feature.settings.mvi import androidx.compose.runtime.Stable +import com.stslex.atten.core.ui.mvi.Store import com.stslex.atten.feature.settings.mvi.SettingsStore.Action import com.stslex.atten.feature.settings.mvi.SettingsStore.Event import com.stslex.atten.feature.settings.mvi.SettingsStore.State -import com.stslex.atten.core.ui.mvi.Store interface SettingsStore : Store { @@ -51,6 +51,9 @@ interface SettingsStore : Store { } @Stable - sealed interface Event : Store.Event + sealed interface Event : Store.Event { + + data object GoogleAuth : Event + } } 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 f18af20..c116639 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,9 +1,10 @@ package com.stslex.atten.feature.settings.mvi.handlers +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 import com.stslex.atten.feature.settings.mvi.SettingsStore.Action -import com.stslex.atten.core.ui.mvi.handler.Handler import org.koin.core.annotation.Factory import org.koin.core.annotation.Scope import org.koin.core.annotation.Scoped @@ -25,6 +26,6 @@ class ClickHandler : Handler { } private fun SettingsHandlerStore.actionLogin() { - // Handle login action + sendEvent(SettingsStore.Event.GoogleAuth) } } \ No newline at end of file diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/ui/SettingsScreen.kt b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/ui/SettingsScreen.kt index 8daaec1..bd7c3f5 100644 --- a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/ui/SettingsScreen.kt +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/ui/SettingsScreen.kt @@ -1,15 +1,37 @@ package com.stslex.atten.feature.settings.ui import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import com.stslex.atten.core.auth.model.AuthEvent +import com.stslex.atten.core.auth.state.GoogleAuthStateImpl.Companion.rememberGoogleAuthState +import com.stslex.atten.core.auth.ui.GoogleAuthInit +import com.stslex.atten.core.core.logger.Log +import com.stslex.atten.core.ui.mvi.NavComponentScreen import com.stslex.atten.feature.settings.di.SettingsFeature import com.stslex.atten.feature.settings.mvi.SettingsComponent -import com.stslex.atten.core.ui.mvi.NavComponentScreen +import com.stslex.atten.feature.settings.mvi.SettingsStore @Composable fun SettingsScreen( component: SettingsComponent ) { NavComponentScreen(SettingsFeature, component) { processor -> + val googleAuthState = rememberGoogleAuthState() + + processor.handle { event -> + when (event) { + SettingsStore.Event.GoogleAuth -> googleAuthState.consume(AuthEvent.Auth) + } + } + + LaunchedEffect(Unit) { + googleAuthState.state.collect { + Log.tag("GOOGLE_AUTH_SETTINGS").d("collected: $it") + } + } + + GoogleAuthInit(googleAuthState) + SettingsWidget( state = processor.state.value, consume = processor::consume, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5e91bdb..02d01a0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -35,6 +35,8 @@ decompose = "3.3.0" essenty = "2.5.0" parcelize = "0.2.4" +gms = "21.3.0" + [libraries] android-gradlePlugin = { module = "com.android.tools.build:gradle", version.ref = "agp" } kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } @@ -86,6 +88,8 @@ kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx- kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "datetime" } +gms-auth = { group = "com.google.android.gms", name = "play-services-auth", version.ref = "gms" } + decompose = { module = "com.arkivanov.decompose:decompose", version.ref = "decompose" } decompose-extensions = { module = "com.arkivanov.decompose:extensions-compose", version.ref = "decompose" } essenty-lifecycle = { module = "com.arkivanov.essenty:lifecycle", version.ref = "essenty" } diff --git a/settings.gradle.kts b/settings.gradle.kts index fadb089..34ef1e4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -26,6 +26,7 @@ include(":core:ui:mvi") include(":core:database") include(":core:paging") include(":core:todo") +include(":core:auth") include(":feature:home") include(":feature:details") From e7190637d3a5465d0d34bb5e23722cd014f3298e Mon Sep 17 00:00:00 2001 From: stslex Date: Wed, 2 Jul 2025 22:55:42 +0300 Subject: [PATCH 3/6] add props to ci for correct signing --- .github/workflows/build_linux.yml | 24 +++++++++++++++++++----- .github/workflows/build_mac_os.yml | 15 +++++++++++++++ 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build_linux.yml b/.github/workflows/build_linux.yml index 01b1730..32310d9 100644 --- a/.github/workflows/build_linux.yml +++ b/.github/workflows/build_linux.yml @@ -18,6 +18,10 @@ jobs: - name: Checkout branch uses: actions/checkout@v2 + - run: | + echo "${{ secrets.KEYSTORE }}" > keystore.jks.gpg --armor --output myfile.txt.asc --detach-sign myfile.txt + gpg -d --passphrase "${{ secrets.KEYSTORE_PASSPHRASE }}" --batch keystore.jks.asc > keystore.jks + - name: set up JDK 17 uses: actions/setup-java@v3 with: @@ -28,11 +32,21 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew - # local properties not needed now - # - name: set up LOCAL_PROPERTIES - # env: - # LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }} - # run: echo "$LOCAL_PROPERTIES" > ./local.properties + - name: Configure Keystore + env: + KEYSTORE_KEY_ALIAS: ${{ secrets.KEYSTORE_KEY_ALIAS }} + KEYSTORE_KEY_PASSWORD: ${{ secrets.KEYSTORE_KEY_PASSWORD }} + KEYSTORE_STORE_PASSWORD: ${{ secrets.KEYSTORE_STORE_PASSWORD }} + run: | + echo "storeFile=keystore.jks" >> keystore.properties + echo "keyAlias=$KEYSTORE_KEY_ALIAS" >> keystore.properties + echo "storePassword=$KEYSTORE_STORE_PASSWORD" >> keystore.properties + echo "keyPassword=$KEYSTORE_KEY_PASSWORD" >> keystore.properties + + - name: set up LOCAL_PROPERTIES + env: + LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }} + run: echo "$LOCAL_PROPERTIES" > ./local.properties - name: Build with Gradle run: ./gradlew assembleAndroidTest \ No newline at end of file diff --git a/.github/workflows/build_mac_os.yml b/.github/workflows/build_mac_os.yml index 349c595..2f876cc 100644 --- a/.github/workflows/build_mac_os.yml +++ b/.github/workflows/build_mac_os.yml @@ -16,6 +16,10 @@ jobs: - name: Checkout branch uses: actions/checkout@v4 + - run: | + echo "${{ secrets.KEYSTORE }}" > keystore.jks.gpg --armor --output myfile.txt.asc --detach-sign myfile.txt + gpg -d --passphrase "${{ secrets.KEYSTORE_PASSPHRASE }}" --batch keystore.jks.asc > keystore.jks + - name: Set up JDK 21 uses: actions/setup-java@v4 with: @@ -28,6 +32,17 @@ jobs: - name: Set Xcode version run: sudo xcode-select -s /Applications/Xcode_15.3.app/Contents/Developer + - name: Configure Keystore + env: + KEYSTORE_KEY_ALIAS: ${{ secrets.KEYSTORE_KEY_ALIAS }} + KEYSTORE_KEY_PASSWORD: ${{ secrets.KEYSTORE_KEY_PASSWORD }} + KEYSTORE_STORE_PASSWORD: ${{ secrets.KEYSTORE_STORE_PASSWORD }} + run: | + echo "storeFile=keystore.jks" >> keystore.properties + echo "keyAlias=$KEYSTORE_KEY_ALIAS" >> keystore.properties + echo "storePassword=$KEYSTORE_STORE_PASSWORD" >> keystore.properties + echo "keyPassword=$KEYSTORE_KEY_PASSWORD" >> keystore.properties + - name: set up LOCAL_PROPERTIES env: LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }} From 140c2042bb749befa85bad39d91463a40475e4d6 Mon Sep 17 00:00:00 2001 From: Ilya Stepanyuk Date: Wed, 2 Jul 2025 23:05:40 +0300 Subject: [PATCH 4/6] Update build_linux.yml --- .github/workflows/build_linux.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_linux.yml b/.github/workflows/build_linux.yml index 32310d9..f330181 100644 --- a/.github/workflows/build_linux.yml +++ b/.github/workflows/build_linux.yml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@v2 - run: | - echo "${{ secrets.KEYSTORE }}" > keystore.jks.gpg --armor --output myfile.txt.asc --detach-sign myfile.txt + echo "${{ secrets.KEYSTORE }}" > keystore.jks.asc gpg -d --passphrase "${{ secrets.KEYSTORE_PASSPHRASE }}" --batch keystore.jks.asc > keystore.jks - name: set up JDK 17 @@ -49,4 +49,4 @@ jobs: run: echo "$LOCAL_PROPERTIES" > ./local.properties - name: Build with Gradle - run: ./gradlew assembleAndroidTest \ No newline at end of file + run: ./gradlew assembleAndroidTest From 1a021391cad09a43e32df2d8f87bcd5674da898e Mon Sep 17 00:00:00 2001 From: Ilya Stepanyuk Date: Wed, 2 Jul 2025 23:06:03 +0300 Subject: [PATCH 5/6] Update build_mac_os.yml --- .github/workflows/build_mac_os.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_mac_os.yml b/.github/workflows/build_mac_os.yml index 2f876cc..146ac09 100644 --- a/.github/workflows/build_mac_os.yml +++ b/.github/workflows/build_mac_os.yml @@ -17,7 +17,7 @@ jobs: uses: actions/checkout@v4 - run: | - echo "${{ secrets.KEYSTORE }}" > keystore.jks.gpg --armor --output myfile.txt.asc --detach-sign myfile.txt + echo "${{ secrets.KEYSTORE }}" > keystore.jks.asc gpg -d --passphrase "${{ secrets.KEYSTORE_PASSPHRASE }}" --batch keystore.jks.asc > keystore.jks - name: Set up JDK 21 @@ -52,4 +52,4 @@ jobs: run: ./gradlew kspKotlinMetadata - name: Build with Gradle - run: cd iosApp && xcodebuild -workspace ./iosApp.xcworkspace -scheme iosApp -configuration Debug -destination 'platform=iOS Simulator,OS=latest,name=iPhone 15' CODE_SIGNING_ALLOWED='NO' \ No newline at end of file + run: cd iosApp && xcodebuild -workspace ./iosApp.xcworkspace -scheme iosApp -configuration Debug -destination 'platform=iOS Simulator,OS=latest,name=iPhone 15' CODE_SIGNING_ALLOWED='NO' From 26bc5640bbb2198892e1aac229526cf374f62d50 Mon Sep 17 00:00:00 2001 From: stslex Date: Thu, 3 Jul 2025 23:22:59 +0300 Subject: [PATCH 6/6] refactor google auth controller --- .../kotlin/com/stslex/atten/MainActivity.kt | 11 ++ .../kotlin/com/stslex/atten/di/AppModules.kt | 6 +- core/auth/build.gradle.kts | 1 + .../GoogleAuthControllerImpl.android.kt | 94 +++++++++++++ .../core/auth/ui/GoogleAuthInit.android.kt | 126 ------------------ .../core/auth/callback/GoogleAuthCallback.kt | 3 + .../auth/callback/GoogleAuthCallbackImpl.kt | 19 +++ .../callback/GoogleAuthReceiverCallback.kt | 8 ++ .../callback/GoogleAuthTransmitterCallback.kt | 8 ++ .../auth/controller/GoogleAuthController.kt | 13 ++ .../controller/GoogleAuthControllerImpl.kt | 17 +++ .../atten/core/auth/di/ModuleCoreAuth.kt | 23 ++++ .../stslex/atten/core/auth/model/AuthEvent.kt | 13 -- .../stslex/atten/core/auth/model/AuthState.kt | 23 ---- .../atten/core/auth/model/GoogleAuthResult.kt | 2 +- .../atten/core/auth/state/GoogleAuthState.kt | 18 --- .../core/auth/state/GoogleAuthStateImpl.kt | 46 ------- .../atten/core/auth/ui/GoogleAuthInit.kt | 16 --- .../GoogleAuthControllerImpl.ios.kt | 21 +++ .../atten/core/auth/ui/GoogleAuthInit.ios.kt | 8 -- .../core/database/di/ModuleCoreDatabase.kt | 5 - .../kit/utils/ActivityHolderImpl.android.kt | 20 +++ .../atten/core/ui/kit/utils/ActivityHolder.kt | 6 + .../core/ui/kit/utils/ActivityHolderImpl.kt | 6 + .../ui/kit/utils/ActivityHolderProducer.kt | 6 + .../core/ui/kit/utils/ModuleCoreUiUtils.kt | 16 +++ .../ui/kit/utils/ActivityHolderImpl.ios.kt | 11 ++ .../core/ui/mvi/processor/EffectsProcessor.kt | 2 +- .../core/ui/mvi/processor/StoreProcessor.kt | 1 - .../ui/mvi/processor/StoreProcessorImpl.kt | 1 - .../feature/settings/mvi/SettingsStore.kt | 5 +- .../settings/mvi/handlers/ClickHandler.kt | 12 +- .../settings/mvi/handlers/LifecycleHandler.kt | 2 +- .../feature/settings/ui/SettingsScreen.kt | 25 +--- 34 files changed, 304 insertions(+), 290 deletions(-) create mode 100644 core/auth/src/androidMain/kotlin/com/stslex/atten/core/auth/controller/GoogleAuthControllerImpl.android.kt delete mode 100644 core/auth/src/androidMain/kotlin/com/stslex/atten/core/auth/ui/GoogleAuthInit.android.kt create mode 100644 core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/callback/GoogleAuthCallback.kt create mode 100644 core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/callback/GoogleAuthCallbackImpl.kt create mode 100644 core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/callback/GoogleAuthReceiverCallback.kt create mode 100644 core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/callback/GoogleAuthTransmitterCallback.kt create mode 100644 core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/controller/GoogleAuthController.kt create mode 100644 core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/controller/GoogleAuthControllerImpl.kt create mode 100644 core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/di/ModuleCoreAuth.kt delete mode 100644 core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/model/AuthEvent.kt delete mode 100644 core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/model/AuthState.kt delete mode 100644 core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/state/GoogleAuthState.kt delete mode 100644 core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/state/GoogleAuthStateImpl.kt delete mode 100644 core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/ui/GoogleAuthInit.kt create mode 100644 core/auth/src/iosMain/kotlin/com/stslex/atten/core/auth/controller/GoogleAuthControllerImpl.ios.kt delete mode 100644 core/auth/src/iosMain/kotlin/com/stslex/atten/core/auth/ui/GoogleAuthInit.ios.kt create mode 100644 core/ui/kit/src/androidMain/kotlin/com/stslex/atten/core/ui/kit/utils/ActivityHolderImpl.android.kt create mode 100644 core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/utils/ActivityHolder.kt create mode 100644 core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/utils/ActivityHolderImpl.kt create mode 100644 core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/utils/ActivityHolderProducer.kt create mode 100644 core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/utils/ModuleCoreUiUtils.kt create mode 100644 core/ui/kit/src/iosMain/kotlin/com/stslex/atten/core/ui/kit/utils/ActivityHolderImpl.ios.kt diff --git a/commonApp/src/androidMain/kotlin/com/stslex/atten/MainActivity.kt b/commonApp/src/androidMain/kotlin/com/stslex/atten/MainActivity.kt index a839a4a..bd8ebe5 100644 --- a/commonApp/src/androidMain/kotlin/com/stslex/atten/MainActivity.kt +++ b/commonApp/src/androidMain/kotlin/com/stslex/atten/MainActivity.kt @@ -10,15 +10,21 @@ import androidx.core.view.WindowCompat import androidx.lifecycle.compose.LocalLifecycleOwner import com.arkivanov.decompose.DefaultComponentContext import com.arkivanov.decompose.defaultComponentContext +import com.stslex.atten.core.ui.kit.utils.ActivityHolderProducer import com.stslex.atten.host.DefaultRootComponent +import org.koin.android.ext.android.getKoin class MainActivity : ComponentActivity() { + private val activityProducer: ActivityHolderProducer by lazy { getKoin().get() } + override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) val rootComponent = DefaultRootComponent(defaultComponentContext()) val windowController = WindowCompat.getInsetsController(window, window.decorView) + + activityProducer.produce(this) setContent { App( rootComponent = rootComponent, @@ -28,6 +34,11 @@ class MainActivity : ComponentActivity() { ) } } + + override fun onDestroy() { + super.onDestroy() + activityProducer.produce(null) + } } @Preview 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 779530c..0be757f 100644 --- a/commonApp/src/commonMain/kotlin/com/stslex/atten/di/AppModules.kt +++ b/commonApp/src/commonMain/kotlin/com/stslex/atten/di/AppModules.kt @@ -1,12 +1,14 @@ package com.stslex.atten.di -import com.stslex.atten.feature.settings.di.ModuleFeatureSettings +import com.stslex.atten.core.auth.di.ModuleCoreAuth import com.stslex.atten.core.core.di.ModuleCore import com.stslex.atten.core.database.di.ModuleCoreDatabase import com.stslex.atten.core.paging.di.ModuleCorePaging 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 import com.stslex.atten.feature.home.di.ModuleFeatureHome +import com.stslex.atten.feature.settings.di.ModuleFeatureSettings import org.koin.core.module.Module import org.koin.ksp.generated.module @@ -15,6 +17,8 @@ val appModules: List = listOf( ModuleCoreDatabase().module, ModuleCoreToDo().module, ModuleCorePaging().module, + ModuleCoreUiUtils().module, + ModuleCoreAuth().module, ModuleFeatureHome().module, ModuleFeatureDetails().module, ModuleFeatureSettings().module diff --git a/core/auth/build.gradle.kts b/core/auth/build.gradle.kts index 456646a..d12c79b 100644 --- a/core/auth/build.gradle.kts +++ b/core/auth/build.gradle.kts @@ -6,6 +6,7 @@ kotlin { sourceSets.apply { commonMain.dependencies { implementation(project(":core:core")) + implementation(project(":core:ui:kit")) } 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 new file mode 100644 index 0000000..27e3413 --- /dev/null +++ b/core/auth/src/androidMain/kotlin/com/stslex/atten/core/auth/controller/GoogleAuthControllerImpl.android.kt @@ -0,0 +1,94 @@ +package com.stslex.atten.core.auth.controller + +import android.app.Activity +import android.content.IntentSender.SendIntentException +import androidx.activity.ComponentActivity +import androidx.activity.compose.ManagedActivityResultLauncher +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.ActivityResult +import androidx.activity.result.IntentSenderRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.Composable +import com.google.android.gms.auth.api.identity.AuthorizationRequest +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.GoogleAuthResult +import com.stslex.atten.core.core.logger.Log +import com.stslex.atten.core.ui.kit.utils.ActivityHolder + +internal actual class GoogleAuthControllerImpl actual constructor( + private val callback: GoogleAuthCallback, + private val activityHolder: ActivityHolder +) : GoogleAuthController { + + private lateinit var launcher: ManagedActivityResultLauncher + private val activity: ComponentActivity + get() = requireNotNull(activityHolder.activity as? ComponentActivity) + + @Composable + actual override fun RegisterLauncher() { + launcher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartIntentSenderForResult() + ) { 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)) + } + callback.process(result) + } + } + + actual override fun auth( + block: (Result) -> Unit + ) { + callback { block(it) } + + val authRequest = AuthorizationRequest.Builder() + .setRequestedScopes(listOf(Scope(Scopes.EMAIL))) + .build() + + Identity.getAuthorizationClient(activity) + .authorize(authRequest) + .addOnSuccessListener { result -> + logger.d("on success: $result") + if (result.hasResolution()) { + try { + val pendingIntent = checkNotNull(result.pendingIntent) + val intentSenderRequest = IntentSenderRequest.Builder(pendingIntent).build() + launcher.launch(intentSenderRequest) + } catch (e: SendIntentException) { + logger.e(e, "Couldn't start Authorization UI: " + e.localizedMessage) + } + } else { + val uiResult = GoogleAuthResult( + accessToken = result.accessToken, + serverAuthCode = result.serverAuthCode + ) + callback.process(Result.success(uiResult)) + } + } + .addOnFailureListener { + logger.e(it) + callback.process(Result.failure(it)) + } + } + + companion object { + + 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/androidMain/kotlin/com/stslex/atten/core/auth/ui/GoogleAuthInit.android.kt b/core/auth/src/androidMain/kotlin/com/stslex/atten/core/auth/ui/GoogleAuthInit.android.kt deleted file mode 100644 index 6b79355..0000000 --- a/core/auth/src/androidMain/kotlin/com/stslex/atten/core/auth/ui/GoogleAuthInit.android.kt +++ /dev/null @@ -1,126 +0,0 @@ -package com.stslex.atten.core.auth.ui - -import android.app.Activity -import android.content.IntentSender.SendIntentException -import androidx.activity.compose.LocalActivity -import androidx.activity.compose.ManagedActivityResultLauncher -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.ActivityResult -import androidx.activity.result.IntentSenderRequest -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.lifecycle.compose.LocalLifecycleOwner -import androidx.lifecycle.coroutineScope -import com.google.android.gms.auth.api.identity.AuthorizationRequest -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.model.AuthEvent -import com.stslex.atten.core.auth.model.GoogleAuthResult -import com.stslex.atten.core.auth.state.GoogleAuthState -import com.stslex.atten.core.core.logger.Log -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach - - -@Composable -internal actual fun GoogleAuthPlatformInit(state: GoogleAuthState) { - val activity = requireNotNull(LocalActivity.current) - val lifecycleOwner = LocalLifecycleOwner.current - - val launcher = rememberLauncher(state::consumeResult) - - DisposableEffect(lifecycleOwner) { - Log.tag("GOOGLE_AUTH").d("launch_auth") - val eventJob = state.event - .onEach { event -> - Log.tag("GOOGLE_AUTH").d("collect: $event") - when (event) { - AuthEvent.Auth -> processAuth( - activity = activity, - launcher = launcher, - processResult = state::consumeResult - ) - - AuthEvent.Cancel -> Unit - } - }.launchIn(lifecycleOwner.lifecycle.coroutineScope) - - onDispose { - Log.tag("GOOGLE_AUTH").d("dispose_auth") - eventJob.cancel() - } - } -} - -@Composable -private fun rememberLauncher( - processResult: (Result) -> Unit -): ManagedActivityResultLauncher = - checkNotNull(LocalActivity.current).let { activity -> - rememberLauncherForActivityResult( - contract = ActivityResultContracts.StartIntentSenderForResult() - ) { result -> - Log.tag("GOOGLE_AUTH").d("activity_result: $result, ${result.data}") - result.data?.extras?.keySet()?.forEach { key -> - Log.tag("GOOGLE_AUTH").d("[$key] = ${result.data?.extras?.get(key)}") - } - 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)) - } - processResult(result) - } - } - - -// todo replace with handler in di (not ui object) -private fun processAuth( - activity: Activity, - launcher: ManagedActivityResultLauncher, - processResult: (Result) -> Unit -) { - val authRequest = AuthorizationRequest.Builder() - .setRequestedScopes(listOf(Scope(Scopes.EMAIL))) - .build() - Identity.getAuthorizationClient(activity) - .authorize(authRequest) - .addOnCanceledListener { - Log.tag("GOOGLE_AUTH").d("addOnCanceledListener") - } - .addOnFailureListener { e -> - Log.tag("GOOGLE_AUTH").e(e, "addOnFailureListener") - processResult(Result.failure(e)) - } - .addOnSuccessListener { result -> - Log.tag("GOOGLE_AUTH").d("on success: $result") - if (result.hasResolution()) { - try { - val pendingIntent = checkNotNull(result.pendingIntent) - val intentSenderRequest = IntentSenderRequest.Builder(pendingIntent).build() - launcher.launch(intentSenderRequest) - } catch (e: SendIntentException) { - Log.e( - e, - "Couldn't start Authorization UI: " + e.localizedMessage - ) - } - } else { - val uiResult = GoogleAuthResult( - accessToken = result.accessToken, - serverAuthCode = result.serverAuthCode - ) - processResult(Result.success(uiResult)) - } - } -} diff --git a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/callback/GoogleAuthCallback.kt b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/callback/GoogleAuthCallback.kt new file mode 100644 index 0000000..696c0a1 --- /dev/null +++ b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/callback/GoogleAuthCallback.kt @@ -0,0 +1,3 @@ +package com.stslex.atten.core.auth.callback + +internal interface GoogleAuthCallback : GoogleAuthReceiverCallback, GoogleAuthTransmitterCallback \ No newline at end of file diff --git a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/callback/GoogleAuthCallbackImpl.kt b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/callback/GoogleAuthCallbackImpl.kt new file mode 100644 index 0000000..a31bb14 --- /dev/null +++ b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/callback/GoogleAuthCallbackImpl.kt @@ -0,0 +1,19 @@ +package com.stslex.atten.core.auth.callback + +import com.stslex.atten.core.auth.model.GoogleAuthResult +import org.koin.core.annotation.Factory + +@Factory +internal class GoogleAuthCallbackImpl : GoogleAuthCallback { + + private var callback: (Result) -> Unit = {} + + override operator fun invoke(block: (Result) -> Unit) { + callback = block + } + + override fun process(result: Result) { + callback.invoke(result) + } +} + diff --git a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/callback/GoogleAuthReceiverCallback.kt b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/callback/GoogleAuthReceiverCallback.kt new file mode 100644 index 0000000..1b2215f --- /dev/null +++ b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/callback/GoogleAuthReceiverCallback.kt @@ -0,0 +1,8 @@ +package com.stslex.atten.core.auth.callback + +import com.stslex.atten.core.auth.model.GoogleAuthResult + +fun interface GoogleAuthReceiverCallback { + + operator fun invoke(block: (Result) -> Unit) +} \ No newline at end of file diff --git a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/callback/GoogleAuthTransmitterCallback.kt b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/callback/GoogleAuthTransmitterCallback.kt new file mode 100644 index 0000000..ebba9fe --- /dev/null +++ b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/callback/GoogleAuthTransmitterCallback.kt @@ -0,0 +1,8 @@ +package com.stslex.atten.core.auth.callback + +import com.stslex.atten.core.auth.model.GoogleAuthResult + +internal interface GoogleAuthTransmitterCallback { + + fun process(result: Result) +} \ 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 new file mode 100644 index 0000000..696bde4 --- /dev/null +++ b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/controller/GoogleAuthController.kt @@ -0,0 +1,13 @@ +package com.stslex.atten.core.auth.controller + +import androidx.compose.runtime.Composable +import com.stslex.atten.core.auth.model.GoogleAuthResult + +interface GoogleAuthController { + + @Composable + fun RegisterLauncher() + + fun auth(block: (Result) -> Unit) +} + diff --git a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/controller/GoogleAuthControllerImpl.kt b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/controller/GoogleAuthControllerImpl.kt new file mode 100644 index 0000000..3472510 --- /dev/null +++ b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/controller/GoogleAuthControllerImpl.kt @@ -0,0 +1,17 @@ +package com.stslex.atten.core.auth.controller + +import androidx.compose.runtime.Composable +import com.stslex.atten.core.auth.callback.GoogleAuthCallback +import com.stslex.atten.core.auth.model.GoogleAuthResult +import com.stslex.atten.core.ui.kit.utils.ActivityHolder + +internal expect class GoogleAuthControllerImpl( + callback: GoogleAuthCallback, + activityHolder: ActivityHolder +) : GoogleAuthController { + + @Composable + override fun RegisterLauncher() + + override fun auth(block: (Result) -> Unit) +} \ No newline at end of file diff --git a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/di/ModuleCoreAuth.kt b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/di/ModuleCoreAuth.kt new file mode 100644 index 0000000..527b7b8 --- /dev/null +++ b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/di/ModuleCoreAuth.kt @@ -0,0 +1,23 @@ +package com.stslex.atten.core.auth.di + +import com.stslex.atten.core.auth.callback.GoogleAuthCallback +import com.stslex.atten.core.auth.controller.GoogleAuthController +import com.stslex.atten.core.auth.controller.GoogleAuthControllerImpl +import com.stslex.atten.core.ui.kit.utils.ActivityHolder +import org.koin.core.annotation.ComponentScan +import org.koin.core.annotation.Module +import org.koin.core.annotation.Single + +@Module +@ComponentScan("com.stslex.atten.core.auth") +class ModuleCoreAuth { + + @Single + internal fun googleAuthController( + callback: GoogleAuthCallback, + holder: ActivityHolder + ): GoogleAuthController = GoogleAuthControllerImpl( + callback = callback, + activityHolder = holder + ) +} \ No newline at end of file diff --git a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/model/AuthEvent.kt b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/model/AuthEvent.kt deleted file mode 100644 index 256a757..0000000 --- a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/model/AuthEvent.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.stslex.atten.core.auth.model - -import androidx.compose.runtime.Stable - -@Stable -sealed interface AuthEvent { - - @Stable - data object Auth : AuthEvent - - @Stable - data object Cancel : AuthEvent -} \ No newline at end of file diff --git a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/model/AuthState.kt b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/model/AuthState.kt deleted file mode 100644 index ddcf6e7..0000000 --- a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/model/AuthState.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.stslex.atten.core.auth.model - -import androidx.compose.runtime.Stable - -@Stable -sealed interface AuthState { - - @Stable - data object Idle : AuthState - - @Stable - data object Loading : AuthState - - @Stable - data class Success( - val result: GoogleAuthResult - ) : AuthState - - @Stable - data class Error( - val error: Throwable - ) : AuthState -} \ No newline at end of file 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 d984133..4e50773 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 @@ -6,4 +6,4 @@ import androidx.compose.runtime.Stable data class GoogleAuthResult( val serverAuthCode: String?, val accessToken: String?, -) \ No newline at end of file +) diff --git a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/state/GoogleAuthState.kt b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/state/GoogleAuthState.kt deleted file mode 100644 index 6ac5518..0000000 --- a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/state/GoogleAuthState.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.stslex.atten.core.auth.state - -import com.stslex.atten.core.auth.model.AuthEvent -import com.stslex.atten.core.auth.model.AuthState -import com.stslex.atten.core.auth.model.GoogleAuthResult -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow - -interface GoogleAuthState { - - val state: StateFlow - - val event: SharedFlow - - suspend fun consume(event: AuthEvent) - - fun consumeResult(result: Result) -} \ No newline at end of file diff --git a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/state/GoogleAuthStateImpl.kt b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/state/GoogleAuthStateImpl.kt deleted file mode 100644 index 1234cf8..0000000 --- a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/state/GoogleAuthStateImpl.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.stslex.atten.core.auth.state - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.remember -import com.stslex.atten.core.auth.model.AuthEvent -import com.stslex.atten.core.auth.model.AuthState -import com.stslex.atten.core.auth.model.GoogleAuthResult -import com.stslex.atten.core.core.logger.Log -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow - -@Immutable -class GoogleAuthStateImpl private constructor() : GoogleAuthState { - - private val _state: MutableStateFlow = MutableStateFlow(AuthState.Idle) - override val state: StateFlow = _state.asStateFlow() - - private val _event: MutableSharedFlow = MutableSharedFlow() - override val event: SharedFlow = _event.asSharedFlow() - - override suspend fun consume(event: AuthEvent) { - logger.d("consume: $event") - _event.emit(event) - } - - override fun consumeResult(result: Result) { - logger.d("result: $result") - result - .onFailure { _state.value = AuthState.Error(it) } - .onSuccess { _state.value = AuthState.Success(it) } - } - - - companion object { - - private val logger = Log.tag("GoogleAuthState") - - @Composable - fun rememberGoogleAuthState(): GoogleAuthState = remember { GoogleAuthStateImpl() } - } -} \ No newline at end of file diff --git a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/ui/GoogleAuthInit.kt b/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/ui/GoogleAuthInit.kt deleted file mode 100644 index 94247e2..0000000 --- a/core/auth/src/commonMain/kotlin/com/stslex/atten/core/auth/ui/GoogleAuthInit.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.stslex.atten.core.auth.ui - -import androidx.compose.runtime.Composable -import com.stslex.atten.core.auth.state.GoogleAuthState - -@Composable -fun GoogleAuthInit( - state: GoogleAuthState, -) { - GoogleAuthPlatformInit(state) -} - -@Composable -internal expect fun GoogleAuthPlatformInit( - state: GoogleAuthState, -) \ No newline at end of file diff --git a/core/auth/src/iosMain/kotlin/com/stslex/atten/core/auth/controller/GoogleAuthControllerImpl.ios.kt b/core/auth/src/iosMain/kotlin/com/stslex/atten/core/auth/controller/GoogleAuthControllerImpl.ios.kt new file mode 100644 index 0000000..ca6eaa7 --- /dev/null +++ b/core/auth/src/iosMain/kotlin/com/stslex/atten/core/auth/controller/GoogleAuthControllerImpl.ios.kt @@ -0,0 +1,21 @@ +package com.stslex.atten.core.auth.controller + +import androidx.compose.runtime.Composable +import com.stslex.atten.core.auth.callback.GoogleAuthCallback +import com.stslex.atten.core.auth.model.GoogleAuthResult +import com.stslex.atten.core.ui.kit.utils.ActivityHolder + +internal actual class GoogleAuthControllerImpl actual constructor( + callback: GoogleAuthCallback, + activityHolder: ActivityHolder +) : GoogleAuthController { + + @Composable + actual override fun RegisterLauncher() { + // do nothing - need for android + } + + actual override fun auth(block: (Result) -> Unit) { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/core/auth/src/iosMain/kotlin/com/stslex/atten/core/auth/ui/GoogleAuthInit.ios.kt b/core/auth/src/iosMain/kotlin/com/stslex/atten/core/auth/ui/GoogleAuthInit.ios.kt deleted file mode 100644 index 63212e2..0000000 --- a/core/auth/src/iosMain/kotlin/com/stslex/atten/core/auth/ui/GoogleAuthInit.ios.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.stslex.atten.core.auth.ui - -import androidx.compose.runtime.Composable -import com.stslex.atten.core.auth.state.GoogleAuthState - -@Composable -internal actual fun GoogleAuthPlatformInit(state: GoogleAuthState) { -} \ No newline at end of file diff --git a/core/database/src/commonMain/kotlin/com/stslex/atten/core/database/di/ModuleCoreDatabase.kt b/core/database/src/commonMain/kotlin/com/stslex/atten/core/database/di/ModuleCoreDatabase.kt index 2112539..bab9f23 100644 --- a/core/database/src/commonMain/kotlin/com/stslex/atten/core/database/di/ModuleCoreDatabase.kt +++ b/core/database/src/commonMain/kotlin/com/stslex/atten/core/database/di/ModuleCoreDatabase.kt @@ -11,11 +11,6 @@ import org.koin.core.scope.Scope @ComponentScan("com.stslex.atten.core.database") class ModuleCoreDatabase { -// val module = module { -// single { getDatabase() } -// single { get().getTodoDao() } -// } - @Single fun appDatabase(scope: Scope): AppDatabase = scope.getDatabase() diff --git a/core/ui/kit/src/androidMain/kotlin/com/stslex/atten/core/ui/kit/utils/ActivityHolderImpl.android.kt b/core/ui/kit/src/androidMain/kotlin/com/stslex/atten/core/ui/kit/utils/ActivityHolderImpl.android.kt new file mode 100644 index 0000000..3f2a125 --- /dev/null +++ b/core/ui/kit/src/androidMain/kotlin/com/stslex/atten/core/ui/kit/utils/ActivityHolderImpl.android.kt @@ -0,0 +1,20 @@ +package com.stslex.atten.core.ui.kit.utils + +import java.lang.ref.WeakReference + +actual class ActivityHolderImpl actual constructor() : ActivityHolder, ActivityHolderProducer { + + private var _activity: WeakReference? = null + + actual override val activity: Any? + get() = _activity?.get() + + actual override fun produce(activity: Any?) { + if (activity == null) { + _activity?.clear() + _activity = null + } else { + _activity = WeakReference(activity) + } + } +} \ No newline at end of file diff --git a/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/utils/ActivityHolder.kt b/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/utils/ActivityHolder.kt new file mode 100644 index 0000000..e0decfd --- /dev/null +++ b/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/utils/ActivityHolder.kt @@ -0,0 +1,6 @@ +package com.stslex.atten.core.ui.kit.utils + +interface ActivityHolder { + + val activity: Any? +} \ No newline at end of file diff --git a/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/utils/ActivityHolderImpl.kt b/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/utils/ActivityHolderImpl.kt new file mode 100644 index 0000000..94610d0 --- /dev/null +++ b/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/utils/ActivityHolderImpl.kt @@ -0,0 +1,6 @@ +package com.stslex.atten.core.ui.kit.utils + +expect class ActivityHolderImpl() : ActivityHolder, ActivityHolderProducer { + override val activity: Any? + override fun produce(activity: Any?) +} \ No newline at end of file diff --git a/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/utils/ActivityHolderProducer.kt b/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/utils/ActivityHolderProducer.kt new file mode 100644 index 0000000..e324346 --- /dev/null +++ b/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/utils/ActivityHolderProducer.kt @@ -0,0 +1,6 @@ +package com.stslex.atten.core.ui.kit.utils + +interface ActivityHolderProducer { + + fun produce(activity: Any?) +} diff --git a/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/utils/ModuleCoreUiUtils.kt b/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/utils/ModuleCoreUiUtils.kt new file mode 100644 index 0000000..a3841f6 --- /dev/null +++ b/core/ui/kit/src/commonMain/kotlin/com/stslex/atten/core/ui/kit/utils/ModuleCoreUiUtils.kt @@ -0,0 +1,16 @@ +package com.stslex.atten.core.ui.kit.utils + +import org.koin.core.annotation.Module +import org.koin.core.annotation.Single + +@Module +class ModuleCoreUiUtils { + + @Single + fun activityHolder(): ActivityHolder = ActivityHolderImpl() + + @Single + fun activityHolderProducer( + holder: ActivityHolder + ): ActivityHolderProducer = holder as ActivityHolderProducer +} \ No newline at end of file diff --git a/core/ui/kit/src/iosMain/kotlin/com/stslex/atten/core/ui/kit/utils/ActivityHolderImpl.ios.kt b/core/ui/kit/src/iosMain/kotlin/com/stslex/atten/core/ui/kit/utils/ActivityHolderImpl.ios.kt new file mode 100644 index 0000000..d3fdb13 --- /dev/null +++ b/core/ui/kit/src/iosMain/kotlin/com/stslex/atten/core/ui/kit/utils/ActivityHolderImpl.ios.kt @@ -0,0 +1,11 @@ +package com.stslex.atten.core.ui.kit.utils + +actual class ActivityHolderImpl actual constructor() : + ActivityHolder, + ActivityHolderProducer { + actual override val activity: Any? + get() = TODO("Not yet implemented") + + actual override fun produce(activity: Any?) { + } +} \ No newline at end of file diff --git a/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/processor/EffectsProcessor.kt b/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/processor/EffectsProcessor.kt index 20d03b6..2b15d74 100644 --- a/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/processor/EffectsProcessor.kt +++ b/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/processor/EffectsProcessor.kt @@ -1,4 +1,4 @@ -package com.stslex.wizard.core.ui.mvi.v2.processor +package com.stslex.atten.core.ui.mvi.processor import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable diff --git a/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/processor/StoreProcessor.kt b/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/processor/StoreProcessor.kt index 8b6cc20..345b607 100644 --- a/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/processor/StoreProcessor.kt +++ b/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/processor/StoreProcessor.kt @@ -11,7 +11,6 @@ import com.stslex.atten.core.ui.mvi.Store.Event import com.stslex.atten.core.ui.mvi.Store.State import com.stslex.atten.core.ui.navigation.Component import com.stslex.wizard.core.ui.mvi.v2.processor.ActionProcessor -import com.stslex.wizard.core.ui.mvi.v2.processor.EffectsProcessor import kotlinx.coroutines.CoroutineScope import org.koin.compose.viewmodel.koinViewModel import org.koin.core.component.KoinScopeComponent diff --git a/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/processor/StoreProcessorImpl.kt b/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/processor/StoreProcessorImpl.kt index 16c7b9b..ec42983 100644 --- a/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/processor/StoreProcessorImpl.kt +++ b/core/ui/mvi/src/commonMain/kotlin/com/stslex/atten/core/ui/mvi/processor/StoreProcessorImpl.kt @@ -5,7 +5,6 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.State import com.stslex.atten.core.ui.mvi.Store import com.stslex.wizard.core.ui.mvi.v2.processor.ActionProcessor -import com.stslex.wizard.core.ui.mvi.v2.processor.EffectsProcessor import kotlinx.coroutines.CoroutineScope @Immutable 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 ac5816b..01403cd 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 @@ -51,9 +51,6 @@ interface SettingsStore : Store { } @Stable - sealed interface Event : Store.Event { - - data object GoogleAuth : Event - } + sealed interface Event : Store.Event } 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 c116639..51f5b49 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,9 +1,9 @@ package com.stslex.atten.feature.settings.mvi.handlers +import com.stslex.atten.core.auth.controller.GoogleAuthController 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 import com.stslex.atten.feature.settings.mvi.SettingsStore.Action import org.koin.core.annotation.Factory import org.koin.core.annotation.Scope @@ -12,7 +12,9 @@ import org.koin.core.annotation.Scoped @Factory @Scope(SettingsScope::class) @Scoped() -class ClickHandler : Handler { +class ClickHandler( + private val authController: GoogleAuthController +) : Handler { override fun SettingsHandlerStore.invoke(action: Action.Click) { when (action) { @@ -26,6 +28,10 @@ class ClickHandler : Handler { } private fun SettingsHandlerStore.actionLogin() { - sendEvent(SettingsStore.Event.GoogleAuth) + authController.auth { result -> + result + .onSuccess { logger.i("success: $it") } + .onFailure { logger.e(it, "auth error") } + } } } \ No newline at end of file diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/handlers/LifecycleHandler.kt b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/handlers/LifecycleHandler.kt index 49e1062..de98454 100644 --- a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/handlers/LifecycleHandler.kt +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/mvi/handlers/LifecycleHandler.kt @@ -1,9 +1,9 @@ package com.stslex.atten.feature.settings.mvi.handlers +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 com.stslex.atten.core.ui.mvi.handler.Handler import kotlinx.coroutines.delay import org.koin.core.annotation.Factory import org.koin.core.annotation.Scope diff --git a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/ui/SettingsScreen.kt b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/ui/SettingsScreen.kt index bd7c3f5..2de4ea8 100644 --- a/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/ui/SettingsScreen.kt +++ b/feature/settings/src/commonMain/kotlin/com/stslex/atten/feature/settings/ui/SettingsScreen.kt @@ -1,37 +1,18 @@ package com.stslex.atten.feature.settings.ui import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import com.stslex.atten.core.auth.model.AuthEvent -import com.stslex.atten.core.auth.state.GoogleAuthStateImpl.Companion.rememberGoogleAuthState -import com.stslex.atten.core.auth.ui.GoogleAuthInit -import com.stslex.atten.core.core.logger.Log +import com.stslex.atten.core.auth.controller.GoogleAuthController import com.stslex.atten.core.ui.mvi.NavComponentScreen import com.stslex.atten.feature.settings.di.SettingsFeature import com.stslex.atten.feature.settings.mvi.SettingsComponent -import com.stslex.atten.feature.settings.mvi.SettingsStore +import org.koin.compose.getKoin @Composable fun SettingsScreen( component: SettingsComponent ) { NavComponentScreen(SettingsFeature, component) { processor -> - val googleAuthState = rememberGoogleAuthState() - - processor.handle { event -> - when (event) { - SettingsStore.Event.GoogleAuth -> googleAuthState.consume(AuthEvent.Auth) - } - } - - LaunchedEffect(Unit) { - googleAuthState.state.collect { - Log.tag("GOOGLE_AUTH_SETTINGS").d("collected: $it") - } - } - - GoogleAuthInit(googleAuthState) - + getKoin().get().RegisterLauncher() SettingsWidget( state = processor.state.value, consume = processor::consume,