diff --git a/.run/Android Install Debug.run.xml b/.run/Android Install Debug.run.xml
deleted file mode 100644
index 22d03d8..0000000
--- a/.run/Android Install Debug.run.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
- true
- true
- false
- false
-
-
-
\ No newline at end of file
diff --git a/.run/Run Android Application.run.xml b/.run/Run Android Application.run.xml
new file mode 100644
index 0000000..ef762cd
--- /dev/null
+++ b/.run/Run Android Application.run.xml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/Run Desktop App.run.xml b/.run/Run Desktop App.run.xml
index e5d3fe1..bdd9641 100644
--- a/.run/Run Desktop App.run.xml
+++ b/.run/Run Desktop App.run.xml
@@ -1,7 +1,7 @@
-
+
diff --git a/build-conventions/build.gradle.kts b/build-conventions/build.gradle.kts
index 7043084..f5d8bb3 100644
--- a/build-conventions/build.gradle.kts
+++ b/build-conventions/build.gradle.kts
@@ -13,6 +13,7 @@ kotlin {
}
dependencies {
+ api(files(libs.javaClass.superclass.protectionDomain.codeSource.location))
api(libs.kotlin.plugin)
api(libs.android.gradle.plugin)
api(libs.compose.multiplatform.plugin)
diff --git a/build-conventions/src/main/kotlin/koin-dependencies-convention.gradle.kts b/build-conventions/src/main/kotlin/koin-dependencies-convention.gradle.kts
index 08faada..738079f 100644
--- a/build-conventions/src/main/kotlin/koin-dependencies-convention.gradle.kts
+++ b/build-conventions/src/main/kotlin/koin-dependencies-convention.gradle.kts
@@ -1,3 +1,5 @@
+import org.gradle.accessors.dm.LibrariesForLibs
+
plugins {
id("multiplatform-library-convention")
id("com.google.devtools.ksp")
@@ -5,16 +7,10 @@ plugins {
version = "SNAPSHOT"
-val koinVersion = "3.5.3"
-val koinAnnotationsVersion = "1.3.0"
+val libs = the()
dependencies {
- commonMainImplementation("io.insert-koin:koin-core:$koinVersion")
- commonMainImplementation("io.insert-koin:koin-annotations:$koinAnnotationsVersion")
-
- val kspCompiler = "io.insert-koin:koin-ksp-compiler:$koinAnnotationsVersion"
-
- add("kspCommonMainMetadata", kspCompiler)
- add("kspJvm", kspCompiler)
- add("kspAndroid", kspCompiler)
+ commonMainImplementation(libs.koin.core)
+ commonMainImplementation(libs.koin.annotations)
+ ksp(libs.koin.ksp.compiler)
}
\ No newline at end of file
diff --git a/build-conventions/src/main/kotlin/multiplatform-library-convention.gradle.kts b/build-conventions/src/main/kotlin/multiplatform-library-convention.gradle.kts
index bb38a70..98f025f 100644
--- a/build-conventions/src/main/kotlin/multiplatform-library-convention.gradle.kts
+++ b/build-conventions/src/main/kotlin/multiplatform-library-convention.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
plugins {
kotlin("multiplatform")
id("com.android.library")
@@ -25,3 +27,8 @@ android {
}
}
+tasks.withType {
+ kotlinOptions {
+ jvmTarget = "19"
+ }
+}
diff --git a/build.gradle.kts b/build.gradle.kts
index e8b72b7..8b2e76a 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -4,6 +4,7 @@ plugins {
alias(libs.plugins.kotlin.multiplatform) apply false
alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.android.library) apply false
+ alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.parcelize) apply false
alias(libs.plugins.ksp) apply false
@@ -16,7 +17,22 @@ group = "org.timemates.app"
allprojects {
tasks.withType {
kotlinOptions {
- jvmTarget = "19"
+ freeCompilerArgs += listOf("-Xskip-prerelease-check")
+
+ val composeReportsDir = "compose_reports"
+
+ freeCompilerArgs += listOf(
+ "-P",
+ "plugin:androidx.compose.compiler.plugins.kotlin:stabilityConfigurationPath=$rootDir/stability-config.txt"
+ )
+ freeCompilerArgs += listOf(
+ "-P",
+ "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
+ project.layout.buildDirectory.get().dir(composeReportsDir).asFile.absolutePath,
+ "-P",
+ "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
+ project.layout.buildDirectory.get().dir(composeReportsDir).asFile.absolutePath,
+ )
}
}
}
diff --git a/localization/build.gradle.kts b/core/localization/build.gradle.kts
similarity index 100%
rename from localization/build.gradle.kts
rename to core/localization/build.gradle.kts
diff --git a/localization/compose/build.gradle.kts b/core/localization/compose/build.gradle.kts
similarity index 64%
rename from localization/compose/build.gradle.kts
rename to core/localization/compose/build.gradle.kts
index 1bc98ee..5c901df 100644
--- a/localization/compose/build.gradle.kts
+++ b/core/localization/compose/build.gradle.kts
@@ -3,5 +3,5 @@ plugins {
}
dependencies {
- commonMainImplementation(projects.localization)
+ commonMainImplementation(projects.core.localization)
}
\ No newline at end of file
diff --git a/localization/compose/src/commonMain/kotlin/org/timemates/app/localization/compose/LocalLocalization.kt b/core/localization/compose/src/commonMain/kotlin/org/timemates/app/localization/compose/LocalLocalization.kt
similarity index 100%
rename from localization/compose/src/commonMain/kotlin/org/timemates/app/localization/compose/LocalLocalization.kt
rename to core/localization/compose/src/commonMain/kotlin/org/timemates/app/localization/compose/LocalLocalization.kt
diff --git a/localization/src/commonMain/kotlin/org/timemates/app/localization/EnglishStrings.kt b/core/localization/src/commonMain/kotlin/org/timemates/app/localization/EnglishStrings.kt
similarity index 81%
rename from localization/src/commonMain/kotlin/org/timemates/app/localization/EnglishStrings.kt
rename to core/localization/src/commonMain/kotlin/org/timemates/app/localization/EnglishStrings.kt
index 3853d54..e87c1c8 100644
--- a/localization/src/commonMain/kotlin/org/timemates/app/localization/EnglishStrings.kt
+++ b/core/localization/src/commonMain/kotlin/org/timemates/app/localization/EnglishStrings.kt
@@ -1,11 +1,5 @@
package org.timemates.app.localization
-import io.timemates.sdk.authorization.sessions.types.value.ConfirmationCode
-import io.timemates.sdk.timers.types.value.TimerDescription
-import io.timemates.sdk.timers.types.value.TimerName
-import io.timemates.sdk.users.profile.types.value.UserDescription
-import io.timemates.sdk.users.profile.types.value.UserName
-
object EnglishStrings : Strings {
override val appName: String = "TimeMates"
override val start: String = "Start"
@@ -23,7 +17,7 @@ object EnglishStrings : Strings {
override val changeEmail: String = "Change email"
override val emailSizeIsInvalid: String = "Email address size should be in range of 5 and 200 symbols"
override val emailIsInvalid: String = "Email address is invalid."
- override val codeSizeIsInvalid: String = "Code size should be ${ConfirmationCode.SIZE} symbols length"
+ override val codeSizeIsInvalid: String = "Code size should be 0 symbols length"
override val codeIsInvalid: String = "Confirmation code should consist only from [a-Z] and [0-9]"
override val unknownFailure: String = "Unknown failure happened"
override val confirmationAttemptFailed: String = "Confirmation code is invalid. Recheck and try again."
@@ -34,9 +28,9 @@ object EnglishStrings : Strings {
override val configureNewAccountDescription: String = "Welcome to TimeMates! Let’s start our journey by configuring your profile details."
override val aboutYou: String = "About you"
override val yourName: String = "Your name"
- override val nameSizeIsInvalid: String = "Name size should be in range of ${UserName.SIZE_RANGE.first} to ${UserName.SIZE_RANGE.last} symbols."
+ override val nameSizeIsInvalid: String = "Name size should be in range of 0 to 0 symbols."
override val nameIsInvalid: String = "Name consists from illegal characters."
- override val aboutYouSizeIsInvalid: String = "User description should be in range of ${UserDescription.SIZE_RANGE.first} and ${UserDescription.SIZE_RANGE.last} symbols."
+ override val aboutYouSizeIsInvalid: String = "User description should be in range of 0 and 0 symbols."
override val timerSettings: String = "Edit timer"
override val description: String = "Description"
override val name: String = "Name"
@@ -47,8 +41,8 @@ object EnglishStrings : Strings {
override val advancedRestSettingsDescription: String = "Enable big rest time (extended rest every X rounds)."
override val publicManageTimerStateDescription: String = "Everyone can manage timer state"
override val confirmationRequiredDescription: String = "Always require confirmation before round start"
- override val timerNameSizeIsInvalid: String = "Name size should be in range of ${TimerName.SIZE_RANGE.first} to ${TimerName.SIZE_RANGE.last} symbols."
- override val timerDescriptionSizeIsInvalid: String = "Timer description should be in range of ${TimerDescription.SIZE_RANGE.first} and ${TimerDescription.SIZE_RANGE.last} symbols."
+ override val timerNameSizeIsInvalid: String = "Name size should be in range of 0 to 0 symbols."
+ override val timerDescriptionSizeIsInvalid: String = "Timer description should be in range of 0 and 0 symbols."
override val save: String = "Save"
override val welcome: String = "Welcome to TimeMates"
override val welcomeDescription: String = "Unlock Your Productivity: Seamlessly Organize Tasks, Collaborate Effortlessly, and Achieve your Goals."
@@ -59,9 +53,28 @@ object EnglishStrings : Strings {
override val alreadyExists: String = "It already exists or functionality isn't supposed to be used twice."
override val invalidArgument: String = "Invalid input information."
override val notFound: String = "Entity is not found."
- override val unauthorized: String = "Your authorized was whether terminated or expired, please relogin."
+ override val unauthorized: String = "Your authorization is whether terminated or expired, please relogin."
override val unavailable: String = "Service is not available at the moment, please try again later."
override val unsupported: String = "This functionality is not yet supported."
+ override val fieldCannotBeEmpty: String = "This field cannot be empty."
+
+ override fun minValueFailure(min: Number): String {
+ return "Minimal value is the $min."
+ }
+
+ override fun lengthExactFailure(size: Int): String {
+ return "Length should be exactly $size."
+ }
+
+ override fun lengthRangeFailure(range: IntRange): String {
+ return "Length should be in a range of ${range.first} to ${range.last}."
+ }
+
+ override fun valueRangeFailure(first: Number, last: Number): String {
+ return "Value range should be in frame from $first to $last."
+ }
+
+ override val patternFailure: String = "The value does not follow the correct format."
override fun internalError(message: String): String {
return "Internal server failure has happened, details: $message"
diff --git a/localization/src/commonMain/kotlin/org/timemates/app/localization/Strings.kt b/core/localization/src/commonMain/kotlin/org/timemates/app/localization/Strings.kt
similarity index 83%
rename from localization/src/commonMain/kotlin/org/timemates/app/localization/Strings.kt
rename to core/localization/src/commonMain/kotlin/org/timemates/app/localization/Strings.kt
index d9d5b37..76f9c1c 100644
--- a/localization/src/commonMain/kotlin/org/timemates/app/localization/Strings.kt
+++ b/core/localization/src/commonMain/kotlin/org/timemates/app/localization/Strings.kt
@@ -1,7 +1,9 @@
package org.timemates.app.localization
+import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
+@Immutable
interface Strings {
val appName: String
@@ -105,6 +107,22 @@ interface Strings {
val unsupported: String
+ val fieldCannotBeEmpty: String
+
+ @Stable
+ fun minValueFailure(min: Number): String
+
+ @Stable
+ fun lengthExactFailure(size: Int): String
+
+ @Stable
+ fun lengthRangeFailure(range: IntRange): String
+
+ @Stable
+ fun valueRangeFailure(first: Number, last: Number): String
+
+ val patternFailure: String
+
@Stable
fun internalError(message: String): String
diff --git a/localization/src/commonMain/kotlin/org/timemates/app/localization/UkrainianStrings.kt b/core/localization/src/commonMain/kotlin/org/timemates/app/localization/UkrainianStrings.kt
similarity index 84%
rename from localization/src/commonMain/kotlin/org/timemates/app/localization/UkrainianStrings.kt
rename to core/localization/src/commonMain/kotlin/org/timemates/app/localization/UkrainianStrings.kt
index 0a02943..1500b06 100644
--- a/localization/src/commonMain/kotlin/org/timemates/app/localization/UkrainianStrings.kt
+++ b/core/localization/src/commonMain/kotlin/org/timemates/app/localization/UkrainianStrings.kt
@@ -1,11 +1,5 @@
package org.timemates.app.localization
-import io.timemates.sdk.authorization.sessions.types.value.ConfirmationCode
-import io.timemates.sdk.timers.types.value.TimerDescription
-import io.timemates.sdk.timers.types.value.TimerName
-import io.timemates.sdk.users.profile.types.value.UserDescription
-import io.timemates.sdk.users.profile.types.value.UserName
-
object UkrainianStrings : Strings {
override val appName: String = "TimeMates"
override val start: String = "Почати"
@@ -23,7 +17,7 @@ object UkrainianStrings : Strings {
override val changeEmail: String = "Змінити пошту"
override val emailSizeIsInvalid: String = "Розмір електронної пошти має бути від 5 до 200 символів"
override val emailIsInvalid: String = "Ваша електронна пошта неправильна."
- override val codeSizeIsInvalid: String = "Розмір має бути ${ConfirmationCode.SIZE} символів."
+ override val codeSizeIsInvalid: String = "Розмір має бути 0 символів."
override val codeIsInvalid: String = "Код має містити лише [a-Z] символи та цифри."
override val unknownFailure: String = "Сталась невідома помилка"
override val confirmationAttemptFailed: String = "Код неправильний, перевірьте будь-ласка код та спробуйте знову."
@@ -34,9 +28,9 @@ object UkrainianStrings : Strings {
override val configureNewAccountDescription: String = "Ласкаво просимо до TimeMates! Давайте почнемо нашу подорож із налаштування даних вашого профілю."
override val aboutYou: String = "Про тебе"
override val yourName: String = "Твоє ім'я"
- override val nameSizeIsInvalid: String = "Ім'я має бути в межах ${UserName.SIZE_RANGE.first} й ${UserName.SIZE_RANGE.last} символів."
+ override val nameSizeIsInvalid: String = "Ім'я має бути в межах 0 й 0 символів."
override val nameIsInvalid: String = "Ім'я містить заборонені символи."
- override val aboutYouSizeIsInvalid: String = "Опис має бути в межах ${UserDescription.SIZE_RANGE.first} й ${UserDescription.SIZE_RANGE.last} символів."
+ override val aboutYouSizeIsInvalid: String = "Опис має бути в межах 0 й 0 символів."
override val timerSettings: String = "Редагувати таймер"
override val description: String = "Описання"
override val name: String = "Найменування"
@@ -47,8 +41,8 @@ object UkrainianStrings : Strings {
override val advancedRestSettingsDescription: String = "Увімкнути подовжений відпочинок (кожні X раундів)."
override val publicManageTimerStateDescription: String = "Кожен може керувати станом таймера"
override val confirmationRequiredDescription: String = "Завжди вимагати підтвердження перед початком раунду"
- override val timerNameSizeIsInvalid: String = "Ім'я має бути в межах ${TimerName.SIZE_RANGE.first} й ${TimerName.SIZE_RANGE.last} символів."
- override val timerDescriptionSizeIsInvalid: String = "Опис має бути в межах ${TimerDescription.SIZE_RANGE.first} й ${TimerDescription.SIZE_RANGE.last} символів."
+ override val timerNameSizeIsInvalid: String = "Ім'я має бути в межах 0 й 0 символів."
+ override val timerDescriptionSizeIsInvalid: String = "Опис має бути в межах 0 й 0 символів."
override val save: String = "Зберегти"
override val welcome: String = "Ласкаво просимо до TimeMates"
override val welcomeDescription: String = "Розкрийте Свою Продуктивність: Легко Організовуйте Завдання, Співпрацюйте Без Зусиль і Досягніть своїх Цілей."
@@ -62,6 +56,25 @@ object UkrainianStrings : Strings {
override val unauthorized: String = "Ваша авторизація була видалена або закінчився строк її дії."
override val unavailable: String = "Сервіс недоступний, спробуйте будь-ласка пізніше."
override val unsupported: String = "Дана функціональність ще не підтримується на сервері або клієнті, зв'яжіться з розробником."
+ override val fieldCannotBeEmpty: String = "Це поле не може бути пустим."
+
+ override fun minValueFailure(min: Number): String {
+ return "Мінімальне значення має відповідати $min."
+ }
+
+ override fun lengthExactFailure(size: Int): String {
+ return "Довжина тексту має бути мінімум $size."
+ }
+
+ override fun lengthRangeFailure(range: IntRange): String {
+ return "Довжина тексту має бути в межах від ${range.first} до ${range.last} символів."
+ }
+
+ override fun valueRangeFailure(first: Number, last: Number): String {
+ return "Діапазон значень має бути в рамках від $first до $last."
+ }
+
+ override val patternFailure: String = "Неправильний формат даних."
override fun internalError(message: String): String {
return "Внутрішня помилка серверу, деталі: $message."
diff --git a/navigation/build.gradle.kts b/core/navigation/build.gradle.kts
similarity index 66%
rename from navigation/build.gradle.kts
rename to core/navigation/build.gradle.kts
index b440af5..b190283 100644
--- a/navigation/build.gradle.kts
+++ b/core/navigation/build.gradle.kts
@@ -4,22 +4,23 @@ plugins {
}
dependencies {
+ commonMainImplementation(projects.core.ui)
+
commonMainImplementation(libs.timemates.sdk)
commonMainApi(libs.decompose)
commonMainApi(libs.decompose.jetbrains.compose)
commonMainImplementation(libs.koin.core)
+ commonMainImplementation(libs.koin.compose)
- commonMainImplementation(projects.styleSystem)
- commonMainImplementation(projects.foundation.mvi)
- commonMainImplementation(projects.foundation.mvi.koinCompose)
+ commonMainImplementation(projects.core.styleSystem)
commonMainImplementation(libs.kotlinx.coroutines)
commonMainImplementation(projects.feature.authorization.presentation)
commonMainImplementation(projects.feature.timers.presentation)
- commonMainImplementation(projects.feature.system.domain)
- commonMainImplementation(projects.feature.system.presentation)
+ commonMainImplementation(projects.feature.splash.domain)
+ commonMainImplementation(projects.feature.splash.presentation)
commonMainImplementation(libs.kotlinx.serialization)
}
\ No newline at end of file
diff --git a/foundation/mvi/koin-compose/src/commonMain/kotlin/org/timemates/app/mvi/compose/stateMachine.kt b/core/navigation/src/commonMain/kotlin/org/timemates/app/navigation/Koin.kt
similarity index 51%
rename from foundation/mvi/koin-compose/src/commonMain/kotlin/org/timemates/app/mvi/compose/stateMachine.kt
rename to core/navigation/src/commonMain/kotlin/org/timemates/app/navigation/Koin.kt
index 4cb2296..6feb83c 100644
--- a/foundation/mvi/koin-compose/src/commonMain/kotlin/org/timemates/app/mvi/compose/stateMachine.kt
+++ b/core/navigation/src/commonMain/kotlin/org/timemates/app/navigation/Koin.kt
@@ -1,20 +1,25 @@
-package org.timemates.app.mvi.compose
+@file:Suppress("USELESS_CAST")
+
+package org.timemates.app.navigation
import androidx.compose.runtime.Composable
-import org.timemates.app.foundation.mvi.StateMachine
+import androidx.compose.runtime.remember
+import org.koin.compose.LocalKoinScope
import org.koin.core.parameter.ParametersDefinition
-
/**
* Creates and returns an instance of the specified state machine using the provided factory.
*
* @param TSM The reified type of the state machine.
- * @param TState The type of the state in the state machine.
- * @param TEvent The type of the events in the state machine.
- * @param TEffect The type of the effects in the state machine.
* @return The created instance of the state machine.
*/
@Composable
-expect inline fun > stateMachine(
+inline fun koinMviComponent(
noinline parameters: ParametersDefinition? = null,
-): TSM
\ No newline at end of file
+): TSM {
+ val koin = LocalKoinScope.current.getKoin()
+
+ return remember {
+ koin.get(qualifier = null, parameters)
+ }
+}
diff --git a/navigation/src/commonMain/kotlin/org/timemates/app/navigation/Navigator.kt b/core/navigation/src/commonMain/kotlin/org/timemates/app/navigation/Navigator.kt
similarity index 84%
rename from navigation/src/commonMain/kotlin/org/timemates/app/navigation/Navigator.kt
rename to core/navigation/src/commonMain/kotlin/org/timemates/app/navigation/Navigator.kt
index b56c700..5d463d0 100644
--- a/navigation/src/commonMain/kotlin/org/timemates/app/navigation/Navigator.kt
+++ b/core/navigation/src/commonMain/kotlin/org/timemates/app/navigation/Navigator.kt
@@ -8,14 +8,14 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.ComponentContext
-import com.arkivanov.decompose.extensions.compose.jetbrains.stack.Children
-import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.StackAnimation
-import com.arkivanov.decompose.extensions.compose.jetbrains.subscribeAsState
+import com.arkivanov.decompose.extensions.compose.stack.Children
+import com.arkivanov.decompose.extensions.compose.stack.animation.StackAnimation
+import com.arkivanov.decompose.extensions.compose.subscribeAsState
+import com.arkivanov.decompose.router.children.NavigationSource
import com.arkivanov.decompose.router.stack.ChildStack
-import com.arkivanov.decompose.router.stack.StackNavigationSource
+import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.childStack
import kotlinx.serialization.KSerializer
-import kotlinx.serialization.builtins.nullable
val LocalComponentContext: ProvidableCompositionLocal =
staticCompositionLocalOf { error("Root component context was not provided") }
@@ -27,7 +27,7 @@ fun ProvideComponentContext(componentContext: ComponentContext, content: @Compos
@Composable
inline fun ChildStack(
- source: StackNavigationSource,
+ source: NavigationSource>,
noinline initialStack: () -> List,
modifier: Modifier = Modifier,
key: String = "DefaultChildStack",
@@ -66,7 +66,7 @@ fun ChildStack(
@Composable
inline fun rememberChildStack(
- source: StackNavigationSource,
+ source: NavigationSource>,
noinline initialStack: () -> List,
key: String = "DefaultChildStack",
): State> {
@@ -74,6 +74,7 @@ inline fun rememberChildStack(
return remember {
componentContext.childStack(
+ key = key,
source = source,
initialStack = initialStack,
handleBackButton = true,
diff --git a/navigation/src/commonMain/kotlin/org/timemates/app/navigation/Screen.kt b/core/navigation/src/commonMain/kotlin/org/timemates/app/navigation/Screen.kt
similarity index 100%
rename from navigation/src/commonMain/kotlin/org/timemates/app/navigation/Screen.kt
rename to core/navigation/src/commonMain/kotlin/org/timemates/app/navigation/Screen.kt
diff --git a/navigation/src/commonMain/kotlin/org/timemates/app/navigation/TimeMatesAppEntry.kt b/core/navigation/src/commonMain/kotlin/org/timemates/app/navigation/TimeMatesAppEntry.kt
similarity index 65%
rename from navigation/src/commonMain/kotlin/org/timemates/app/navigation/TimeMatesAppEntry.kt
rename to core/navigation/src/commonMain/kotlin/org/timemates/app/navigation/TimeMatesAppEntry.kt
index bd1cf90..f1f354d 100644
--- a/navigation/src/commonMain/kotlin/org/timemates/app/navigation/TimeMatesAppEntry.kt
+++ b/core/navigation/src/commonMain/kotlin/org/timemates/app/navigation/TimeMatesAppEntry.kt
@@ -1,44 +1,45 @@
package org.timemates.app.navigation
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
-import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.fade
-import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.plus
-import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.scale
-import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.stackAnimation
+import androidx.compose.ui.Modifier
+import com.arkivanov.decompose.extensions.compose.stack.animation.fade
+import com.arkivanov.decompose.extensions.compose.stack.animation.plus
+import com.arkivanov.decompose.extensions.compose.stack.animation.scale
+import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.pop
import com.arkivanov.decompose.router.stack.popTo
import com.arkivanov.decompose.router.stack.push
import com.arkivanov.decompose.router.stack.replaceAll
+import kotlinx.coroutines.channels.ReceiveChannel
+import kotlinx.coroutines.channels.consumeEach
+import org.koin.core.parameter.parametersOf
import org.timemates.app.authorization.ui.afterstart.AfterStartScreen
-import org.timemates.app.authorization.ui.afterstart.mvi.AfterStartStateMachine
+import org.timemates.app.authorization.ui.afterstart.mvi.AfterStartScreenComponent
import org.timemates.app.authorization.ui.configure_account.ConfigureAccountScreen
-import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountStateMachine
+import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountScreenComponent
import org.timemates.app.authorization.ui.confirmation.ConfirmAuthorizationScreen
-import org.timemates.app.authorization.ui.confirmation.mvi.ConfirmAuthorizationStateMachine
+import org.timemates.app.authorization.ui.confirmation.mvi.ConfirmAuthorizationScreenComponent
import org.timemates.app.authorization.ui.initial_authorization.InitialAuthorizationScreen
-import org.timemates.app.authorization.ui.initial_authorization.mvi.InitialAuthorizationStateMachine
+import org.timemates.app.authorization.ui.initial_authorization.mvi.InitialAuthorizationComponent
import org.timemates.app.authorization.ui.new_account_info.NewAccountInfoScreen
-import org.timemates.app.authorization.ui.new_account_info.mvi.NewAccountInfoStateMachine
+import org.timemates.app.authorization.ui.new_account_info.mvi.NewAccountInfoScreenComponent
import org.timemates.app.authorization.ui.start.StartAuthorizationScreen
-import org.timemates.app.authorization.ui.start.mvi.StartAuthorizationStateMachine
+import org.timemates.app.authorization.ui.start.mvi.StartAuthorizationComponent
import org.timemates.app.feature.common.startup.StartupScreen
-import org.timemates.app.feature.common.startup.mvi.StartupStateMachine
-import org.timemates.app.mvi.compose.stateMachine
+import org.timemates.app.feature.common.startup.mvi.StartupScreenMVIComponent
import org.timemates.app.timers.ui.settings.TimerSettingsScreen
-import org.timemates.app.timers.ui.settings.mvi.TimerSettingsStateMachine
+import org.timemates.app.timers.ui.settings.mvi.TimerSettingsScreenComponent
import org.timemates.app.timers.ui.timer_creation.TimerCreationScreen
-import org.timemates.app.timers.ui.timer_creation.mvi.TimerCreationStateMachine
+import org.timemates.app.timers.ui.timer_creation.mvi.TimerCreationScreenComponent
import org.timemates.app.timers.ui.timers_list.TimersListScreen
-import org.timemates.app.timers.ui.timers_list.mvi.TimersListStateMachine
-import io.timemates.sdk.authorization.email.types.value.VerificationHash
-import io.timemates.sdk.common.constructor.createOrThrow
-import io.timemates.sdk.common.exceptions.UnauthorizedException
-import kotlinx.coroutines.channels.ReceiveChannel
-import kotlinx.coroutines.channels.consumeEach
-import org.koin.core.parameter.parametersOf
+import org.timemates.app.timers.ui.timers_list.mvi.TimersListScreenComponent
+import org.timemates.sdk.authorization.email.types.value.VerificationHash
+import org.timemates.sdk.common.constructor.createOrThrow
+import org.timemates.sdk.common.exceptions.UnauthorizedException
@Composable
fun TimeMatesAppEntry(
@@ -53,20 +54,25 @@ fun TimeMatesAppEntry(
}
ChildStack(
+ modifier = Modifier.fillMaxSize(),
source = navigation,
initialStack = { listOf(initialScreen) },
animation = stackAnimation(fade() + scale()),
) { screen ->
+ val componentContext = LocalComponentContext.current
+
when (screen) {
is Screen.Startup -> StartupScreen(
- stateMachine = stateMachine(),
+ mvi = koinMviComponent {
+ parametersOf(componentContext)
+ },
navigateToAuth = { navigation.push(Screen.InitialAuthorizationScreen) },
navigateToHome = { navigation.replaceAll(Screen.TimersList) },
)
is Screen.ConfirmAuthorization -> ConfirmAuthorizationScreen(
- stateMachine = stateMachine {
- parametersOf(VerificationHash.createOrThrow(screen.verificationHash))
+ mvi = koinMviComponent {
+ parametersOf(componentContext, VerificationHash.factory.createOrThrow(screen.verificationHash))
},
onBack = { navigation.pop() },
navigateToConfiguring = {
@@ -78,22 +84,28 @@ fun TimeMatesAppEntry(
)
Screen.InitialAuthorizationScreen -> InitialAuthorizationScreen(
- stateMachine = stateMachine(),
+ mvi = koinMviComponent {
+ parametersOf(
+ componentContext,
+ )
+ },
navigateToStartAuthorization = {
navigation.push(Screen.StartAuthorization)
},
)
Screen.StartAuthorization -> StartAuthorizationScreen(
- stateMachine = stateMachine(),
+ mvi = koinMviComponent {
+ parametersOf(componentContext)
+ },
onNavigateToConfirmation = {
navigation.push(Screen.AfterStart(it.string))
},
)
is Screen.AfterStart -> AfterStartScreen(
- stateMachine = stateMachine {
- parametersOf(VerificationHash.createOrThrow(screen.verificationHash))
+ mvi = koinMviComponent {
+ parametersOf(componentContext, VerificationHash.factory.createOrThrow(screen.verificationHash))
},
navigateToConfirmation = {
navigation.push(Screen.ConfirmAuthorization(screen.verificationHash))
@@ -104,8 +116,8 @@ fun TimeMatesAppEntry(
)
is Screen.NewAccountInfo -> NewAccountInfoScreen(
- stateMachine = stateMachine {
- parametersOf(VerificationHash.createOrThrow(screen.verificationHash))
+ mvi = koinMviComponent {
+ parametersOf(componentContext, VerificationHash.factory.createOrThrow(screen.verificationHash))
},
navigateToConfigure = {
navigation.push(Screen.NewAccount(screen.verificationHash))
@@ -116,8 +128,8 @@ fun TimeMatesAppEntry(
)
is Screen.NewAccount -> ConfigureAccountScreen(
- stateMachine = stateMachine {
- parametersOf(VerificationHash.createOrThrow(screen.verificationHash))
+ mvi = koinMviComponent {
+ parametersOf(componentContext, VerificationHash.factory.createOrThrow(screen.verificationHash))
},
onBack = {
navigation.popTo(0)
@@ -128,7 +140,9 @@ fun TimeMatesAppEntry(
)
is Screen.TimersList -> TimersListScreen(
- stateMachine = stateMachine(),
+ mvi = koinMviComponent {
+ parametersOf(componentContext)
+ },
navigateToSetting = {
// TODO when settings page is ready
},
@@ -141,14 +155,18 @@ fun TimeMatesAppEntry(
)
is Screen.TimerCreation -> TimerCreationScreen(
- stateMachine = stateMachine(),
+ mvi = koinMviComponent {
+ parametersOf(componentContext)
+ },
navigateToTimersScreen = {
navigation.pop()
},
)
is Screen.TimerSettings -> TimerSettingsScreen(
- stateMachine = stateMachine(),
+ mvi = koinMviComponent {
+ parametersOf(componentContext)
+ },
navigateToTimersScreen = {
navigation.pop()
},
diff --git a/style-system/build.gradle.kts b/core/style-system/build.gradle.kts
similarity index 100%
rename from style-system/build.gradle.kts
rename to core/style-system/build.gradle.kts
diff --git a/style-system/src/commonMain/kotlin/org/timemates/app/style/system/appbar/AppBar.kt b/core/style-system/src/commonMain/kotlin/org/timemates/app/style/system/appbar/AppBar.kt
similarity index 100%
rename from style-system/src/commonMain/kotlin/org/timemates/app/style/system/appbar/AppBar.kt
rename to core/style-system/src/commonMain/kotlin/org/timemates/app/style/system/appbar/AppBar.kt
diff --git a/style-system/src/commonMain/kotlin/org/timemates/app/style/system/button/Button.kt b/core/style-system/src/commonMain/kotlin/org/timemates/app/style/system/button/Button.kt
similarity index 98%
rename from style-system/src/commonMain/kotlin/org/timemates/app/style/system/button/Button.kt
rename to core/style-system/src/commonMain/kotlin/org/timemates/app/style/system/button/Button.kt
index 9b72e31..d310922 100644
--- a/style-system/src/commonMain/kotlin/org/timemates/app/style/system/button/Button.kt
+++ b/core/style-system/src/commonMain/kotlin/org/timemates/app/style/system/button/Button.kt
@@ -37,7 +37,7 @@ fun Button(
) {
androidx.compose.material3.Button(
onClick = onClick,
- modifier = modifier.height(42.dp),
+ modifier = modifier,
enabled = enabled,
shape = MaterialTheme.shapes.small,
colors = colors,
diff --git a/style-system/src/commonMain/kotlin/org/timemates/app/style/system/button/Fab.kt b/core/style-system/src/commonMain/kotlin/org/timemates/app/style/system/button/Fab.kt
similarity index 100%
rename from style-system/src/commonMain/kotlin/org/timemates/app/style/system/button/Fab.kt
rename to core/style-system/src/commonMain/kotlin/org/timemates/app/style/system/button/Fab.kt
diff --git a/style-system/src/commonMain/kotlin/org/timemates/app/style/system/images/CircleIcon.kt b/core/style-system/src/commonMain/kotlin/org/timemates/app/style/system/images/CircleIcon.kt
similarity index 100%
rename from style-system/src/commonMain/kotlin/org/timemates/app/style/system/images/CircleIcon.kt
rename to core/style-system/src/commonMain/kotlin/org/timemates/app/style/system/images/CircleIcon.kt
diff --git a/style-system/src/commonMain/kotlin/org/timemates/app/style/system/text_field/SizedOutlinedTextField.kt b/core/style-system/src/commonMain/kotlin/org/timemates/app/style/system/text_field/SizedOutlinedTextField.kt
similarity index 94%
rename from style-system/src/commonMain/kotlin/org/timemates/app/style/system/text_field/SizedOutlinedTextField.kt
rename to core/style-system/src/commonMain/kotlin/org/timemates/app/style/system/text_field/SizedOutlinedTextField.kt
index d422332..eae6542 100644
--- a/style-system/src/commonMain/kotlin/org/timemates/app/style/system/text_field/SizedOutlinedTextField.kt
+++ b/core/style-system/src/commonMain/kotlin/org/timemates/app/style/system/text_field/SizedOutlinedTextField.kt
@@ -1,7 +1,6 @@
package org.timemates.app.style.system.text_field
import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
@@ -13,7 +12,6 @@ import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldColors
-import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@@ -45,7 +43,7 @@ fun SizedOutlinedTextField(
maxLines: Int = Int.MAX_VALUE,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = OutlinedTextFieldDefaults.shape,
- colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors(),
+ colors: TextFieldColors = OutlinedTextFieldDefaults.colors(),
) {
OutlinedTextField(
value = value,
diff --git a/style-system/src/commonMain/kotlin/org/timemates/app/style/system/theme/AppColors.kt b/core/style-system/src/commonMain/kotlin/org/timemates/app/style/system/theme/AppColors.kt
similarity index 100%
rename from style-system/src/commonMain/kotlin/org/timemates/app/style/system/theme/AppColors.kt
rename to core/style-system/src/commonMain/kotlin/org/timemates/app/style/system/theme/AppColors.kt
diff --git a/style-system/src/commonMain/kotlin/org/timemates/app/style/system/theme/AppTheme.kt b/core/style-system/src/commonMain/kotlin/org/timemates/app/style/system/theme/AppTheme.kt
similarity index 100%
rename from style-system/src/commonMain/kotlin/org/timemates/app/style/system/theme/AppTheme.kt
rename to core/style-system/src/commonMain/kotlin/org/timemates/app/style/system/theme/AppTheme.kt
diff --git a/style-system/src/commonMain/kotlin/org/timemates/app/style/system/theme/AppTypography.kt b/core/style-system/src/commonMain/kotlin/org/timemates/app/style/system/theme/AppTypography.kt
similarity index 100%
rename from style-system/src/commonMain/kotlin/org/timemates/app/style/system/theme/AppTypography.kt
rename to core/style-system/src/commonMain/kotlin/org/timemates/app/style/system/theme/AppTypography.kt
diff --git a/style-system/src/commonMain/kotlin/org/timemates/app/style/system/theme/MaterialColorsAdapter.kt b/core/style-system/src/commonMain/kotlin/org/timemates/app/style/system/theme/MaterialColorsAdapter.kt
similarity index 100%
rename from style-system/src/commonMain/kotlin/org/timemates/app/style/system/theme/MaterialColorsAdapter.kt
rename to core/style-system/src/commonMain/kotlin/org/timemates/app/style/system/theme/MaterialColorsAdapter.kt
diff --git a/style-system/src/commonMain/kotlin/org/timemates/app/style/system/theme/MaterialThemeAdapter.kt b/core/style-system/src/commonMain/kotlin/org/timemates/app/style/system/theme/MaterialThemeAdapter.kt
similarity index 100%
rename from style-system/src/commonMain/kotlin/org/timemates/app/style/system/theme/MaterialThemeAdapter.kt
rename to core/style-system/src/commonMain/kotlin/org/timemates/app/style/system/theme/MaterialThemeAdapter.kt
diff --git a/platforms/desktop/src/main/resources/fonts/Inter-Black.ttf b/core/style-system/src/commonMain/libres/fonts/Inter-Black.ttf
similarity index 100%
rename from platforms/desktop/src/main/resources/fonts/Inter-Black.ttf
rename to core/style-system/src/commonMain/libres/fonts/Inter-Black.ttf
diff --git a/platforms/desktop/src/main/resources/fonts/Inter-Bold.ttf b/core/style-system/src/commonMain/libres/fonts/Inter-Bold.ttf
similarity index 100%
rename from platforms/desktop/src/main/resources/fonts/Inter-Bold.ttf
rename to core/style-system/src/commonMain/libres/fonts/Inter-Bold.ttf
diff --git a/platforms/desktop/src/main/resources/fonts/Inter-ExtraBold.ttf b/core/style-system/src/commonMain/libres/fonts/Inter-ExtraBold.ttf
similarity index 100%
rename from platforms/desktop/src/main/resources/fonts/Inter-ExtraBold.ttf
rename to core/style-system/src/commonMain/libres/fonts/Inter-ExtraBold.ttf
diff --git a/platforms/desktop/src/main/resources/fonts/Inter-ExtraLight.ttf b/core/style-system/src/commonMain/libres/fonts/Inter-ExtraLight.ttf
similarity index 100%
rename from platforms/desktop/src/main/resources/fonts/Inter-ExtraLight.ttf
rename to core/style-system/src/commonMain/libres/fonts/Inter-ExtraLight.ttf
diff --git a/platforms/desktop/src/main/resources/fonts/Inter-Light.ttf b/core/style-system/src/commonMain/libres/fonts/Inter-Light.ttf
similarity index 100%
rename from platforms/desktop/src/main/resources/fonts/Inter-Light.ttf
rename to core/style-system/src/commonMain/libres/fonts/Inter-Light.ttf
diff --git a/platforms/desktop/src/main/resources/fonts/Inter-Medium.ttf b/core/style-system/src/commonMain/libres/fonts/Inter-Medium.ttf
similarity index 100%
rename from platforms/desktop/src/main/resources/fonts/Inter-Medium.ttf
rename to core/style-system/src/commonMain/libres/fonts/Inter-Medium.ttf
diff --git a/platforms/desktop/src/main/resources/fonts/Inter-Regular.ttf b/core/style-system/src/commonMain/libres/fonts/Inter-Regular.ttf
similarity index 100%
rename from platforms/desktop/src/main/resources/fonts/Inter-Regular.ttf
rename to core/style-system/src/commonMain/libres/fonts/Inter-Regular.ttf
diff --git a/platforms/desktop/src/main/resources/fonts/Inter-SemiBold.ttf b/core/style-system/src/commonMain/libres/fonts/Inter-SemiBold.ttf
similarity index 100%
rename from platforms/desktop/src/main/resources/fonts/Inter-SemiBold.ttf
rename to core/style-system/src/commonMain/libres/fonts/Inter-SemiBold.ttf
diff --git a/platforms/desktop/src/main/resources/fonts/Inter-Thin.ttf b/core/style-system/src/commonMain/libres/fonts/Inter-Thin.ttf
similarity index 100%
rename from platforms/desktop/src/main/resources/fonts/Inter-Thin.ttf
rename to core/style-system/src/commonMain/libres/fonts/Inter-Thin.ttf
diff --git a/platforms/desktop/src/main/resources/images/app_icon.png b/core/style-system/src/commonMain/libres/images/app_icon.png
similarity index 100%
rename from platforms/desktop/src/main/resources/images/app_icon.png
rename to core/style-system/src/commonMain/libres/images/app_icon.png
diff --git a/platforms/desktop/src/main/resources/images/confirm_authorization_info_image.svg b/core/style-system/src/commonMain/libres/images/confirm_authorization_info_image.svg
similarity index 100%
rename from platforms/desktop/src/main/resources/images/confirm_authorization_info_image.svg
rename to core/style-system/src/commonMain/libres/images/confirm_authorization_info_image.svg
diff --git a/style-system/src/commonMain/libres/images/empty_list_image.svg b/core/style-system/src/commonMain/libres/images/empty_list_image.svg
similarity index 100%
rename from style-system/src/commonMain/libres/images/empty_list_image.svg
rename to core/style-system/src/commonMain/libres/images/empty_list_image.svg
diff --git a/platforms/desktop/src/main/resources/images/initial_screen_image.svg b/core/style-system/src/commonMain/libres/images/initial_screen_image.svg
similarity index 100%
rename from platforms/desktop/src/main/resources/images/initial_screen_image.svg
rename to core/style-system/src/commonMain/libres/images/initial_screen_image.svg
diff --git a/platforms/desktop/src/main/resources/images/new_account_info_image.svg b/core/style-system/src/commonMain/libres/images/new_account_info_image.svg
similarity index 100%
rename from platforms/desktop/src/main/resources/images/new_account_info_image.svg
rename to core/style-system/src/commonMain/libres/images/new_account_info_image.svg
diff --git a/core/types/serializable/build.gradle.kts b/core/types/serializable/build.gradle.kts
new file mode 100644
index 0000000..8b99c2d
--- /dev/null
+++ b/core/types/serializable/build.gradle.kts
@@ -0,0 +1,11 @@
+plugins {
+ id(libs.plugins.configurations.multiplatform.library.get().pluginId)
+ alias(libs.plugins.kotlinx.serialization)
+}
+
+dependencies {
+ commonMainApi(libs.timemates.sdk)
+ commonMainApi(libs.kotlinx.serialization)
+
+ commonMainApi(libs.kotlinx.datetime)
+}
diff --git a/core/types/serializable/src/commonMain/kotlin/org/timemates/app/core/types/serializable/SerializableTimer.kt b/core/types/serializable/src/commonMain/kotlin/org/timemates/app/core/types/serializable/SerializableTimer.kt
new file mode 100644
index 0000000..4616511
--- /dev/null
+++ b/core/types/serializable/src/commonMain/kotlin/org/timemates/app/core/types/serializable/SerializableTimer.kt
@@ -0,0 +1,146 @@
+package org.timemates.app.core.types.serializable
+
+import kotlinx.datetime.Instant
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import org.timemates.sdk.timers.types.Timer
+import org.timemates.sdk.timers.types.TimerSettings
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.minutes
+
+@Serializable
+data class SerializableTimer(
+ val timerId: Long,
+ val name: String,
+ val description: String,
+ val ownerId: Long,
+ val membersCount: Int,
+ val state: State,
+ val settings: Settings,
+) {
+ @Serializable
+ sealed class State {
+ abstract val endsAt: Instant?
+ abstract val publishTime: Instant
+
+ /**
+ * Represents a paused state of the TimeMates entity.
+ * Paused states do not have an exact time to be expired and are usually paused by force
+ * for an indefinite amount of time. They can be resumed only on purpose. The server may
+ * decide to expire paused states after some time, but the client shouldn't focus on that
+ * and should handle the state accordingly.
+ *
+ * @property publishTime The time when the paused state was published.
+ */
+ @SerialName("Paused")
+ data class Paused(
+ override val publishTime: Instant,
+ ) : State() {
+ override val endsAt: Instant? = null
+ }
+
+ @SerialName("ConfirmationWaiting")
+ data class ConfirmationWaiting(
+ override val endsAt: Instant,
+ override val publishTime: Instant,
+ ) : State()
+
+ /**
+ * Represents an inactive state of the TimeMates entity.
+ *
+ * @property publishTime The time when the inactive state was published.
+ */
+ @SerialName("Inactive")
+ data class Inactive(
+ override val publishTime: Instant,
+ ) : State() {
+ override val endsAt: Instant? = null
+ }
+
+ /**
+ * Represents a running state of the TimeMates entity.
+ *
+ * @property endsAt The time when the running state will lose its actuality.
+ * @property publishTime The time when the running state was published.
+ */
+ @SerialName("Running")
+ data class Running(
+ override val endsAt: Instant,
+ override val publishTime: Instant,
+ ) : State()
+
+ /**
+ * Represents a rest state of the TimeMates entity.
+ *
+ * @property endsAt The time when the rest state will lose its actuality.
+ * @property publishTime The time when the rest state was published.
+ */
+ @SerialName("Rest")
+ data class Rest(
+ override val endsAt: Instant,
+ override val publishTime: Instant,
+ ) : State()
+ }
+
+ @Serializable
+ data class Settings(
+ val workTime: Duration = 25.minutes,
+ val restTime: Duration = 5.minutes,
+ val bigRestTime: Duration = 10.minutes,
+ val bigRestEnabled: Boolean = true,
+ val bigRestPer: Int = 4,
+ val isEveryoneCanPause: Boolean = false,
+ val isConfirmationRequired: Boolean = false,
+ )
+}
+
+fun Timer.serializable(): SerializableTimer {
+ return SerializableTimer(
+ timerId = timerId.long,
+ name = name.string,
+ description = description.string,
+ ownerId = ownerId.long,
+ membersCount = membersCount.int,
+ state = state.serializable(),
+ settings = settings.serializable(),
+ )
+}
+
+fun Timer.State.serializable(): SerializableTimer.State {
+ return when (this) {
+ is Timer.State.ConfirmationWaiting -> SerializableTimer.State.ConfirmationWaiting(
+ endsAt = endsAt,
+ publishTime = publishTime,
+ )
+
+ is Timer.State.Inactive -> SerializableTimer.State.Inactive(
+ publishTime = publishTime,
+ )
+
+ is Timer.State.Paused -> SerializableTimer.State.Paused(
+ publishTime = publishTime,
+ )
+
+ is Timer.State.Rest -> SerializableTimer.State.Rest(
+ endsAt = endsAt,
+ publishTime = publishTime,
+ )
+
+ is Timer.State.Running -> SerializableTimer.State.Running(
+ endsAt = endsAt,
+ publishTime = publishTime,
+ )
+ }
+}
+
+fun TimerSettings.serializable(): SerializableTimer.Settings {
+ return SerializableTimer.Settings(
+ workTime = workTime,
+ restTime = restTime,
+ bigRestTime = bigRestTime,
+ bigRestEnabled = bigRestEnabled,
+ bigRestPer = bigRestPer.int,
+ isEveryoneCanPause = isEveryoneCanPause,
+ isConfirmationRequired = isConfirmationRequired,
+ )
+}
\ No newline at end of file
diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts
new file mode 100644
index 0000000..115ef6a
--- /dev/null
+++ b/core/ui/build.gradle.kts
@@ -0,0 +1,21 @@
+plugins {
+ id(libs.plugins.configurations.compose.multiplatform.get().pluginId)
+ id(libs.plugins.configurations.unit.tests.mockable.get().pluginId)
+ alias(libs.plugins.kotlinx.serialization)
+}
+
+dependencies {
+ commonMainApi(projects.core.styleSystem)
+
+ commonMainApi(projects.core.localization)
+ commonMainApi(projects.core.localization.compose)
+
+ commonMainApi(libs.timemates.sdk)
+ commonMainApi(libs.bundles.presentation)
+
+ commonMainApi(projects.foundation.time)
+}
+
+android {
+ namespace = "org.timemates.app.core.ui"
+}
\ No newline at end of file
diff --git a/core/ui/src/commonMain/kotlin/org/timemates/app/feature/common/Input.kt b/core/ui/src/commonMain/kotlin/org/timemates/app/feature/common/Input.kt
new file mode 100644
index 0000000..09693a3
--- /dev/null
+++ b/core/ui/src/commonMain/kotlin/org/timemates/app/feature/common/Input.kt
@@ -0,0 +1,67 @@
+package org.timemates.app.feature.common
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable
+import org.timemates.app.feature.common.failures.getRepresentative
+import org.timemates.app.localization.Strings
+import org.timemates.sdk.common.constructor.CreationFailure
+import org.timemates.sdk.common.constructor.Factory
+import org.timemates.sdk.common.constructor.results.SafeCreationResult
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.contract
+
+@Immutable
+sealed interface Input {
+ val value: TRaw
+
+ data class Invalid(
+ override val value: TRaw,
+ val failures: List,
+ ) : Input
+
+ @JvmInline
+ value class Valid(override val value: TRaw) : Input
+
+ @JvmInline
+ value class Unknown(override val value: TRaw) : Input
+
+ @Stable
+ fun validated(factory: Factory): Input {
+ return when (val result = factory.createSafe(value)) {
+ is SafeCreationResult.Invalid -> Invalid(value, result.failures)
+ is SafeCreationResult.Valid -> Valid(value)
+ }
+ }
+}
+
+@Stable
+fun input(value: TRaw): Input = Input.Unknown(value)
+
+@OptIn(ExperimentalContracts::class)
+@Stable
+fun Input.isValid(): Boolean {
+ contract {
+ returns(true) implies (this@isValid is Input.Valid)
+ returns(false) implies (this@isValid !is Input.Valid)
+ }
+
+ return this is Input.Valid
+}
+
+@OptIn(ExperimentalContracts::class)
+@Stable
+fun Input.isInvalid(): Boolean {
+ contract {
+ returns(true) implies (this@isInvalid is Input.Invalid)
+ returns(false) implies (this@isInvalid !is Input.Invalid)
+ }
+
+ return this is Input.Invalid
+}
+
+@Stable
+fun Input.getFailuresIfPresent(strings: Strings): String? {
+ return if (isInvalid())
+ failures.getRepresentative(strings)
+ else null
+}
\ No newline at end of file
diff --git a/core/ui/src/commonMain/kotlin/org/timemates/app/feature/common/MVI.kt b/core/ui/src/commonMain/kotlin/org/timemates/app/feature/common/MVI.kt
new file mode 100644
index 0000000..3f61932
--- /dev/null
+++ b/core/ui/src/commonMain/kotlin/org/timemates/app/feature/common/MVI.kt
@@ -0,0 +1,9 @@
+package org.timemates.app.feature.common
+
+import com.arkivanov.decompose.ComponentContext
+import pro.respawn.flowmvi.api.Container
+import pro.respawn.flowmvi.api.MVIAction
+import pro.respawn.flowmvi.api.MVIIntent
+import pro.respawn.flowmvi.api.MVIState
+
+interface MVI : Container, ComponentContext
\ No newline at end of file
diff --git a/feature/common/presentation/src/commonMain/kotlin/org/timemates/app/feature/common/failures/CommonFailureMessage.kt b/core/ui/src/commonMain/kotlin/org/timemates/app/feature/common/failures/CommonFailureMessage.kt
similarity index 62%
rename from feature/common/presentation/src/commonMain/kotlin/org/timemates/app/feature/common/failures/CommonFailureMessage.kt
rename to core/ui/src/commonMain/kotlin/org/timemates/app/feature/common/failures/CommonFailureMessage.kt
index a8880e1..03c2632 100644
--- a/feature/common/presentation/src/commonMain/kotlin/org/timemates/app/feature/common/failures/CommonFailureMessage.kt
+++ b/core/ui/src/commonMain/kotlin/org/timemates/app/feature/common/failures/CommonFailureMessage.kt
@@ -1,14 +1,15 @@
package org.timemates.app.feature.common.failures
+import androidx.compose.runtime.Stable
import org.timemates.app.localization.Strings
-import io.timemates.sdk.common.exceptions.AlreadyExistsException
-import io.timemates.sdk.common.exceptions.InvalidArgumentException
-import io.timemates.sdk.common.exceptions.NotFoundException
-import io.timemates.sdk.common.exceptions.TimeMatesException
-import io.timemates.sdk.common.exceptions.TooManyRequestsException
-import io.timemates.sdk.common.exceptions.UnauthorizedException
-import io.timemates.sdk.common.exceptions.UnavailableException
-import io.timemates.sdk.common.exceptions.UnsupportedException
+import org.timemates.sdk.common.exceptions.AlreadyExistsException
+import org.timemates.sdk.common.exceptions.InvalidArgumentException
+import org.timemates.sdk.common.exceptions.NotFoundException
+import org.timemates.sdk.common.exceptions.TimeMatesException
+import org.timemates.sdk.common.exceptions.TooManyRequestsException
+import org.timemates.sdk.common.exceptions.UnauthorizedException
+import org.timemates.sdk.common.exceptions.UnavailableException
+import org.timemates.sdk.common.exceptions.UnsupportedException
/**
* Parses given [Throwable] and converts it into translated message
@@ -17,6 +18,7 @@ import io.timemates.sdk.common.exceptions.UnsupportedException
* Should be used only when documented failures are already handled and you need
* to display the least informative message to user.
*/
+@Stable
fun Throwable.getDefaultDisplayMessage(strings: Strings): String {
if (this !is TimeMatesException)
return strings.unknownFailure
diff --git a/feature/common/domain/src/commonMain/kotlin/org/timemates/app/feature/common/handler/OnAuthorizationFailedHandler.kt b/core/ui/src/commonMain/kotlin/org/timemates/app/feature/common/failures/OnAuthorizationFailedHandler.kt
similarity index 80%
rename from feature/common/domain/src/commonMain/kotlin/org/timemates/app/feature/common/handler/OnAuthorizationFailedHandler.kt
rename to core/ui/src/commonMain/kotlin/org/timemates/app/feature/common/failures/OnAuthorizationFailedHandler.kt
index 06f672b..0430088 100644
--- a/feature/common/domain/src/commonMain/kotlin/org/timemates/app/feature/common/handler/OnAuthorizationFailedHandler.kt
+++ b/core/ui/src/commonMain/kotlin/org/timemates/app/feature/common/failures/OnAuthorizationFailedHandler.kt
@@ -1,6 +1,6 @@
-package org.timemates.app.feature.common.handler
+package org.timemates.app.feature.common.failures
-import io.timemates.sdk.common.exceptions.UnauthorizedException
+import org.timemates.sdk.common.exceptions.UnauthorizedException
/**
* Interface for handling authorization failures.
diff --git a/core/ui/src/commonMain/kotlin/org/timemates/app/feature/common/failures/ValidationExt.kt b/core/ui/src/commonMain/kotlin/org/timemates/app/feature/common/failures/ValidationExt.kt
new file mode 100644
index 0000000..ffaa71c
--- /dev/null
+++ b/core/ui/src/commonMain/kotlin/org/timemates/app/feature/common/failures/ValidationExt.kt
@@ -0,0 +1,25 @@
+package org.timemates.app.feature.common.failures
+
+import androidx.compose.runtime.Stable
+import org.timemates.app.localization.Strings
+import org.timemates.sdk.common.constructor.CreationFailure
+
+@Stable
+fun CreationFailure.getCommonDisplayMessage(strings: Strings): String {
+ return when (this) {
+ is CreationFailure.BlankValueFailure -> strings.fieldCannotBeEmpty
+ is CreationFailure.MinValueFailure<*> -> strings.minValueFailure(size)
+ is CreationFailure.PatternFailure -> strings.patternFailure
+ is CreationFailure.LengthExactFailure -> strings.lengthExactFailure(size)
+ is CreationFailure.LengthRangeFailure -> strings.lengthRangeFailure(range)
+ is CreationFailure.ValueRangeFailure<*> ->
+ strings.valueRangeFailure(range.start, range.endInclusive)
+
+ is CreationFailure.CompoundFailure -> error("stub!")
+ }
+}
+
+@Stable
+fun List.getRepresentative(strings: Strings): String {
+ return joinToString("\n") { it.getCommonDisplayMessage(strings) }
+}
\ No newline at end of file
diff --git a/core/ui/src/commonMain/kotlin/org/timemates/app/feature/common/providable/LocalTimeProvider.kt b/core/ui/src/commonMain/kotlin/org/timemates/app/feature/common/providable/LocalTimeProvider.kt
new file mode 100644
index 0000000..f92850f
--- /dev/null
+++ b/core/ui/src/commonMain/kotlin/org/timemates/app/feature/common/providable/LocalTimeProvider.kt
@@ -0,0 +1,8 @@
+package org.timemates.app.feature.common.providable
+
+import androidx.compose.runtime.staticCompositionLocalOf
+import org.timemates.app.foundation.time.TimeProvider
+
+val LocalTimeProvider = staticCompositionLocalOf {
+ error("TimeProvider was not provided.")
+}
\ No newline at end of file
diff --git a/feature/common/presentation/src/jvmTest/kotlin/org/timemates/app/common/mvi/middleware/AuthorizationFailureMiddlewareTest.kt b/core/ui/src/jvmTestOld/kotlin/org/timemates/app/common/mvi/middleware/AuthorizationFailureMiddlewareTest.kt
similarity index 73%
rename from feature/common/presentation/src/jvmTest/kotlin/org/timemates/app/common/mvi/middleware/AuthorizationFailureMiddlewareTest.kt
rename to core/ui/src/jvmTestOld/kotlin/org/timemates/app/common/mvi/middleware/AuthorizationFailureMiddlewareTest.kt
index fa25c92..fb1c341 100644
--- a/feature/common/presentation/src/jvmTest/kotlin/org/timemates/app/common/mvi/middleware/AuthorizationFailureMiddlewareTest.kt
+++ b/core/ui/src/jvmTestOld/kotlin/org/timemates/app/common/mvi/middleware/AuthorizationFailureMiddlewareTest.kt
@@ -3,15 +3,13 @@ package org.timemates.app.common.mvi.middleware
import io.mockk.clearAllMocks
import io.mockk.mockk
import io.mockk.verify
-import org.timemates.app.feature.common.middleware.AuthorizationFailureMiddleware.AuthorizationFailureEffect
-import org.timemates.app.feature.common.handler.OnAuthorizationFailedHandler
+import kotlinx.coroutines.flow.MutableStateFlow
+import org.timemates.app.feature.common.failures.OnAuthorizationFailedHandler
import org.timemates.app.feature.common.middleware.AuthorizationFailureMiddleware
-import org.timemates.app.foundation.mvi.StateStore
+import org.timemates.app.feature.common.middleware.AuthorizationFailureMiddleware.AuthorizationFailureEffect
import org.timemates.app.foundation.mvi.UiEffect
import org.timemates.app.foundation.mvi.UiState
-import io.timemates.sdk.common.exceptions.UnauthorizedException
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
+import org.timemates.sdk.common.exceptions.UnauthorizedException
import kotlin.test.AfterTest
import kotlin.test.Test
import kotlin.test.assertEquals
@@ -20,7 +18,6 @@ class AuthorizationFailureMiddlewareTest {
private val onAuthorizationFailed: OnAuthorizationFailedHandler = mockk(relaxed = true)
private val middleware = AuthorizationFailureMiddleware(onAuthorizationFailed)
- private val stateFlow: MutableStateFlow = MutableStateFlow(TestState.Initial)
private sealed class TestEffect : UiEffect {
data class AuthorizationFailure(
@@ -46,7 +43,7 @@ class AuthorizationFailureMiddlewareTest {
val effect = TestEffect.AuthorizationFailure(exception)
// WHEN
- middleware.onEffect(effect, createStateStore())
+ middleware.onEffect(effect, TestState.Initial)
// THEN
verify(exactly = 1) { onAuthorizationFailed.onFailed(exception) }
@@ -58,16 +55,9 @@ class AuthorizationFailureMiddlewareTest {
val effect = TestEffect.AnyOther
// WHEN
- val result = middleware.onEffect(effect, createStateStore())
+ val result = middleware.onEffect(effect, TestState.Initial)
// THEN
assertEquals(expected = TestState.Initial, actual = result)
}
-
- private fun createStateStore(): StateStore {
- return object : StateStore {
- override val state: StateFlow
- get() = stateFlow
- }
- }
}
diff --git a/feature/authorization/data/src/commonMain/kotlin/org/timemates/app/authorization/data/AuthorizationsRepository.kt b/feature/authorization/data/src/commonMain/kotlin/org/timemates/app/authorization/data/AuthorizationsRepository.kt
index 57879b0..b342d40 100644
--- a/feature/authorization/data/src/commonMain/kotlin/org/timemates/app/authorization/data/AuthorizationsRepository.kt
+++ b/feature/authorization/data/src/commonMain/kotlin/org/timemates/app/authorization/data/AuthorizationsRepository.kt
@@ -1,22 +1,22 @@
package org.timemates.app.authorization.data
import org.timemates.app.authorization.data.database.AccountDatabaseQueries
-import io.timemates.credentials.CredentialsStorage
-import io.timemates.sdk.authorization.email.EmailAuthorizationApi
-import io.timemates.sdk.authorization.email.requests.ConfigureNewAccountRequest
-import io.timemates.sdk.authorization.email.requests.ConfirmAuthorizationRequest
-import io.timemates.sdk.authorization.email.types.value.VerificationHash
-import io.timemates.sdk.authorization.sessions.AuthorizedSessionsApi
-import io.timemates.sdk.authorization.sessions.types.Authorization
-import io.timemates.sdk.authorization.sessions.types.value.ApplicationName
-import io.timemates.sdk.authorization.sessions.types.value.ClientIpAddress
-import io.timemates.sdk.authorization.sessions.types.value.ClientVersion
-import io.timemates.sdk.authorization.sessions.types.value.ConfirmationCode
-import io.timemates.sdk.common.constructor.createOrThrow
-import io.timemates.sdk.common.exceptions.UnsupportedException
-import io.timemates.sdk.users.profile.types.value.EmailAddress
-import io.timemates.sdk.users.profile.types.value.UserDescription
-import io.timemates.sdk.users.profile.types.value.UserName
+import org.timemates.credentials.CredentialsStorage
+import org.timemates.sdk.authorization.email.EmailAuthorizationApi
+import org.timemates.sdk.authorization.email.requests.ConfigureNewAccountRequest
+import org.timemates.sdk.authorization.email.requests.ConfirmAuthorizationRequest
+import org.timemates.sdk.authorization.email.types.value.VerificationHash
+import org.timemates.sdk.authorization.sessions.AuthorizedSessionsApi
+import org.timemates.sdk.authorization.sessions.types.Authorization
+import org.timemates.sdk.authorization.sessions.types.value.ApplicationName
+import org.timemates.sdk.authorization.sessions.types.value.ClientIpAddress
+import org.timemates.sdk.authorization.sessions.types.value.ClientVersion
+import org.timemates.sdk.authorization.sessions.types.value.ConfirmationCode
+import org.timemates.sdk.common.constructor.createOrThrow
+import org.timemates.sdk.common.exceptions.UnsupportedException
+import org.timemates.sdk.users.profile.types.value.EmailAddress
+import org.timemates.sdk.users.profile.types.value.UserDescription
+import org.timemates.sdk.users.profile.types.value.UserName
import org.timemates.app.authorization.repositories.AuthorizationsRepository as AuthorizationRepositoryContract
class AuthorizationsRepository(
@@ -37,7 +37,7 @@ class AuthorizationsRepository(
}
override suspend fun authorize(emailAddress: EmailAddress): Result {
- return emailAuthApi.authorize(emailAddress, Authorization.Metadata(appName, appVersion, ClientIpAddress.createOrThrow("UNDEFINED")))
+ return emailAuthApi.authorize(emailAddress, Authorization.Metadata(appName, appVersion, ClientIpAddress.factory.createOrThrow("UNDEFINED")))
}
override suspend fun confirm(verificationHash: VerificationHash, code: ConfirmationCode): Result {
@@ -94,5 +94,5 @@ class AuthorizationsRepository(
}
-private val appName = ApplicationName.createOrThrow("TimeMates App")
-private val appVersion = ClientVersion.createOrThrow(1.0)
\ No newline at end of file
+private val appName = ApplicationName.factory.createOrThrow("TimeMates App")
+private val appVersion = ClientVersion.factory.createOrThrow(1.0)
\ No newline at end of file
diff --git a/feature/authorization/data/src/commonMain/kotlin/org/timemates/app/authorization/data/DatabaseAccessHashProvider.kt b/feature/authorization/data/src/commonMain/kotlin/org/timemates/app/authorization/data/DatabaseAccessHashProvider.kt
index e553c06..ed4472b 100644
--- a/feature/authorization/data/src/commonMain/kotlin/org/timemates/app/authorization/data/DatabaseAccessHashProvider.kt
+++ b/feature/authorization/data/src/commonMain/kotlin/org/timemates/app/authorization/data/DatabaseAccessHashProvider.kt
@@ -1,11 +1,11 @@
package org.timemates.app.authorization.data
import org.timemates.app.authorization.data.database.AccountDatabaseQueries
-import io.timemates.credentials.CredentialsStorage
-import io.timemates.sdk.authorization.types.value.AccessHash
-import io.timemates.sdk.common.constructor.createOrThrow
-import io.timemates.sdk.common.exceptions.UnauthorizedException
-import io.timemates.sdk.common.providers.AccessHashProvider
+import org.timemates.credentials.CredentialsStorage
+import org.timemates.sdk.authorization.types.value.AccessHash
+import org.timemates.sdk.common.constructor.createOrThrow
+import org.timemates.sdk.common.exceptions.UnauthorizedException
+import org.timemates.sdk.common.providers.AccessHashProvider
class DatabaseAccessHashProvider(
private val localQueries: AccountDatabaseQueries,
@@ -15,7 +15,7 @@ class DatabaseAccessHashProvider(
// TODO cache in memory
return localQueries.getCurrent().executeAsOneOrNull()
?.let {
- AccessHash.createOrThrow(
+ AccessHash.factory.createOrThrow(
credentialsStorage.getString("access_hash_${it.id}")
?: throw UnauthorizedException("Authorization wasn't saved to system credentials.")
)
diff --git a/feature/authorization/data/src/commonMain/kotlin/org/timemates/app/authorization/data/DbAuthorizationMapper.kt b/feature/authorization/data/src/commonMain/kotlin/org/timemates/app/authorization/data/DbAuthorizationMapper.kt
index 0170c6b..3a2ad16 100644
--- a/feature/authorization/data/src/commonMain/kotlin/org/timemates/app/authorization/data/DbAuthorizationMapper.kt
+++ b/feature/authorization/data/src/commonMain/kotlin/org/timemates/app/authorization/data/DbAuthorizationMapper.kt
@@ -1,12 +1,12 @@
package org.timemates.app.authorization.data
-import io.timemates.credentials.CredentialsStorage
-import io.timemates.sdk.authorization.sessions.types.Authorization
-import io.timemates.sdk.authorization.types.value.HashValue
-import io.timemates.sdk.common.constructor.createOrThrow
-import io.timemates.sdk.common.exceptions.UnauthorizedException
-import io.timemates.sdk.users.profile.types.value.UserId
import kotlinx.datetime.Instant
+import org.timemates.credentials.CredentialsStorage
+import org.timemates.sdk.authorization.sessions.types.Authorization
+import org.timemates.sdk.authorization.types.value.HashValue
+import org.timemates.sdk.common.constructor.createOrThrow
+import org.timemates.sdk.common.exceptions.UnauthorizedException
+import org.timemates.sdk.users.profile.types.value.UserId
import org.timemates.app.authorization.data.database.Authorization as DbAuthorization
class DbAuthorizationMapper(
@@ -17,17 +17,17 @@ class DbAuthorizationMapper(
): Authorization = with(dbAuthorization) {
return@with Authorization(
accessHash = Authorization.Hash(
- HashValue.createOrThrow(credentialsStorage.getString("access_hash_$id")
+ HashValue.factory.createOrThrow(credentialsStorage.getString("access_hash_$id")
?: throw UnauthorizedException("Authorization wasn't saved to system credentials.")),
Instant.fromEpochMilliseconds(accessHashExpiresAt),
),
refreshHash = Authorization.Hash(
- HashValue.createOrThrow(credentialsStorage.getString("refresh_hash_$id")
+ HashValue.factory.createOrThrow(credentialsStorage.getString("refresh_hash_$id")
?: throw UnauthorizedException("Authorization wasn't saved to system credentials.")),
Instant.fromEpochMilliseconds(accessHashExpiresAt),
),
generationTime = Instant.fromEpochMilliseconds(generationTime),
- userId = UserId.createOrThrow(userId),
+ userId = UserId.factory.createOrThrow(userId),
metadata = null, // TODO when metadata will be implemented on server
)
}
diff --git a/feature/authorization/dependencies/build.gradle.kts b/feature/authorization/dependencies/build.gradle.kts
index 503d246..04cf3e3 100644
--- a/feature/authorization/dependencies/build.gradle.kts
+++ b/feature/authorization/dependencies/build.gradle.kts
@@ -24,7 +24,7 @@ dependencies {
commonMainImplementation(projects.feature.authorization.data)
commonMainImplementation(projects.feature.authorization.data.database)
- commonMainImplementation(projects.feature.common.domain)
+ commonMainImplementation(projects.core.ui)
commonMainImplementation(libs.timemates.credentials.manager)
}
\ No newline at end of file
diff --git a/feature/authorization/dependencies/src/commonMain/kotlin/org/timemates/app/authorization/dependencies/AuthorizationDataModule.kt b/feature/authorization/dependencies/src/commonMain/kotlin/org/timemates/app/authorization/dependencies/AuthorizationDataModule.kt
index 5992a87..93fa692 100644
--- a/feature/authorization/dependencies/src/commonMain/kotlin/org/timemates/app/authorization/dependencies/AuthorizationDataModule.kt
+++ b/feature/authorization/dependencies/src/commonMain/kotlin/org/timemates/app/authorization/dependencies/AuthorizationDataModule.kt
@@ -1,29 +1,29 @@
package org.timemates.app.authorization.dependencies
import app.cash.sqldelight.db.SqlDriver
-import org.timemates.app.authorization.data.DatabaseAccessHashProvider
-import org.timemates.app.authorization.data.DbAuthorizationMapper
-import org.timemates.app.authorization.repositories.AuthorizationsRepository
-import io.timemates.credentials.CredentialsStorage
import io.timemates.data.database.TimeMatesAuthorizations
-import io.timemates.sdk.authorization.email.EmailAuthorizationApi
-import io.timemates.sdk.authorization.sessions.AuthorizedSessionsApi
-import io.timemates.sdk.common.engine.TimeMatesRequestsEngine
-import io.timemates.sdk.common.providers.AccessHashProvider
-import org.koin.core.annotation.Factory
import org.koin.core.annotation.Module
import org.koin.core.annotation.Named
+import org.koin.core.annotation.Singleton
+import org.timemates.app.authorization.data.DatabaseAccessHashProvider
+import org.timemates.app.authorization.data.DbAuthorizationMapper
+import org.timemates.app.authorization.repositories.AuthorizationsRepository
+import org.timemates.credentials.CredentialsStorage
+import org.timemates.sdk.authorization.email.EmailAuthorizationApi
+import org.timemates.sdk.authorization.sessions.AuthorizedSessionsApi
+import org.timemates.sdk.common.engine.TimeMatesRequestsEngine
+import org.timemates.sdk.common.providers.AccessHashProvider
import org.timemates.app.authorization.data.AuthorizationsRepository as AuthorizationsRepositoryImpl
@Module
class AuthorizationDataModule {
- @Factory
+ @Singleton
fun accountsDatabase(@Named("authorization") sqlDriver: SqlDriver): TimeMatesAuthorizations {
return TimeMatesAuthorizations(sqlDriver)
}
- @Factory
+ @Singleton
fun accessHashProvider(
dbAuthorizations: TimeMatesAuthorizations,
credentialsStorage: CredentialsStorage,
@@ -31,7 +31,7 @@ class AuthorizationDataModule {
return DatabaseAccessHashProvider(dbAuthorizations.accountDatabaseQueries, credentialsStorage)
}
- @Factory
+ @Singleton
fun authorizationRepository(
requestsEngine: TimeMatesRequestsEngine,
accessHashProvider: AccessHashProvider,
diff --git a/feature/authorization/dependencies/src/commonMain/kotlin/org/timemates/app/authorization/dependencies/AuthorizationModule.kt b/feature/authorization/dependencies/src/commonMain/kotlin/org/timemates/app/authorization/dependencies/AuthorizationModule.kt
new file mode 100644
index 0000000..698b5fb
--- /dev/null
+++ b/feature/authorization/dependencies/src/commonMain/kotlin/org/timemates/app/authorization/dependencies/AuthorizationModule.kt
@@ -0,0 +1,23 @@
+package org.timemates.app.authorization.dependencies
+
+import org.koin.core.annotation.Module
+import org.timemates.app.authorization.dependencies.screens.AfterStartModule
+import org.timemates.app.authorization.dependencies.screens.ConfigureAccountModule
+import org.timemates.app.authorization.dependencies.screens.ConfirmAuthorizationModule
+import org.timemates.app.authorization.dependencies.screens.InitialAuthorizationModule
+import org.timemates.app.authorization.dependencies.screens.NewAccountInfoModule
+import org.timemates.app.authorization.dependencies.screens.StartAuthorizationModule
+
+@Module(
+ includes = [
+ AuthorizationDataModule::class,
+ // Screen-related
+ AfterStartModule::class,
+ ConfigureAccountModule::class,
+ ConfirmAuthorizationModule::class,
+ InitialAuthorizationModule::class,
+ NewAccountInfoModule::class,
+ StartAuthorizationModule::class,
+ ],
+)
+class AuthorizationModule
\ No newline at end of file
diff --git a/feature/authorization/dependencies/src/commonMain/kotlin/org/timemates/app/authorization/dependencies/screens/AfterStartModule.kt b/feature/authorization/dependencies/src/commonMain/kotlin/org/timemates/app/authorization/dependencies/screens/AfterStartModule.kt
index be63e26..f03e5bb 100644
--- a/feature/authorization/dependencies/src/commonMain/kotlin/org/timemates/app/authorization/dependencies/screens/AfterStartModule.kt
+++ b/feature/authorization/dependencies/src/commonMain/kotlin/org/timemates/app/authorization/dependencies/screens/AfterStartModule.kt
@@ -1,22 +1,22 @@
package org.timemates.app.authorization.dependencies.screens
-import org.timemates.app.authorization.dependencies.AuthorizationDataModule
-import org.timemates.app.authorization.ui.afterstart.mvi.AfterStartReducer
-import org.timemates.app.authorization.ui.afterstart.mvi.AfterStartStateMachine
-import io.timemates.sdk.authorization.email.types.value.VerificationHash
+import com.arkivanov.decompose.ComponentContext
import org.koin.core.annotation.Factory
import org.koin.core.annotation.Module
+import org.timemates.app.authorization.dependencies.AuthorizationDataModule
+import org.timemates.app.authorization.ui.afterstart.mvi.AfterStartScreenComponent
+import org.timemates.sdk.authorization.email.types.value.VerificationHash
@Module(includes = [AuthorizationDataModule::class])
class AfterStartModule {
@Factory
- fun stateMachine(
+ fun mviComponent(
+ componentContext: ComponentContext,
verificationHash: VerificationHash,
- ): AfterStartStateMachine {
- return AfterStartStateMachine(
- reducer = AfterStartReducer(
- verificationHash = verificationHash
- ),
+ ): AfterStartScreenComponent {
+ return AfterStartScreenComponent(
+ componentContext = componentContext,
+ verificationHash = verificationHash,
)
}
}
\ No newline at end of file
diff --git a/feature/authorization/dependencies/src/commonMain/kotlin/org/timemates/app/authorization/dependencies/screens/ConfigureAccountModule.kt b/feature/authorization/dependencies/src/commonMain/kotlin/org/timemates/app/authorization/dependencies/screens/ConfigureAccountModule.kt
index dbcc171..999c24b 100644
--- a/feature/authorization/dependencies/src/commonMain/kotlin/org/timemates/app/authorization/dependencies/screens/ConfigureAccountModule.kt
+++ b/feature/authorization/dependencies/src/commonMain/kotlin/org/timemates/app/authorization/dependencies/screens/ConfigureAccountModule.kt
@@ -1,53 +1,32 @@
package org.timemates.app.authorization.dependencies.screens
+import com.arkivanov.decompose.ComponentContext
+import org.koin.core.annotation.Factory
+import org.koin.core.annotation.Module
import org.timemates.app.authorization.dependencies.AuthorizationDataModule
import org.timemates.app.authorization.repositories.AuthorizationsRepository
-import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountMiddleware
-import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountReducer
-import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountStateMachine
+import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountScreenComponent
import org.timemates.app.authorization.usecases.CreateNewAccountUseCase
-import org.timemates.app.authorization.validation.UserDescriptionValidator
-import org.timemates.app.authorization.validation.UserNameValidator
-import io.timemates.sdk.authorization.email.types.value.VerificationHash
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import org.koin.core.annotation.Factory
-import org.koin.core.annotation.Module
-import org.koin.core.annotation.Singleton
+import org.timemates.sdk.authorization.email.types.value.VerificationHash
@Module(includes = [AuthorizationDataModule::class])
class ConfigureAccountModule {
- @Singleton
- fun configureAccountMiddleware(): ConfigureAccountMiddleware = ConfigureAccountMiddleware()
-
@Factory
fun createNewAccountUseCase(
authorizationsRepository: AuthorizationsRepository,
): CreateNewAccountUseCase = CreateNewAccountUseCase(authorizationsRepository)
- @Singleton
- fun userNameValidator(): UserNameValidator = UserNameValidator()
-
- @Singleton
- fun userDescriptionValidator(): UserDescriptionValidator = UserDescriptionValidator()
-
@Factory
- fun stateMachine(
+ fun mviComponent(
+ componentContext: ComponentContext,
verificationHash: VerificationHash,
- configureAccountMiddleware: ConfigureAccountMiddleware,
createNewAccountUseCase: CreateNewAccountUseCase,
- userNameValidator: UserNameValidator,
- userDescriptionValidator: UserDescriptionValidator,
- ): ConfigureAccountStateMachine {
- return ConfigureAccountStateMachine(
- reducer = ConfigureAccountReducer(
- verificationHash = verificationHash,
- createNewAccountUseCase = createNewAccountUseCase,
- userNameValidator = userNameValidator,
- userDescriptionValidator = userDescriptionValidator,
- ),
- configureAccountMiddleware,
+ ): ConfigureAccountScreenComponent {
+ return ConfigureAccountScreenComponent(
+ componentContext = componentContext,
+ verificationHash = verificationHash,
+ createNewAccountUseCase = createNewAccountUseCase,
)
}
}
\ No newline at end of file
diff --git a/feature/authorization/dependencies/src/commonMain/kotlin/org/timemates/app/authorization/dependencies/screens/ConfirmAuthorizationModule.kt b/feature/authorization/dependencies/src/commonMain/kotlin/org/timemates/app/authorization/dependencies/screens/ConfirmAuthorizationModule.kt
index a6b2000..ffb5ece 100644
--- a/feature/authorization/dependencies/src/commonMain/kotlin/org/timemates/app/authorization/dependencies/screens/ConfirmAuthorizationModule.kt
+++ b/feature/authorization/dependencies/src/commonMain/kotlin/org/timemates/app/authorization/dependencies/screens/ConfirmAuthorizationModule.kt
@@ -1,47 +1,32 @@
package org.timemates.app.authorization.dependencies.screens
+import com.arkivanov.decompose.ComponentContext
+import org.koin.core.annotation.Factory
+import org.koin.core.annotation.Module
import org.timemates.app.authorization.dependencies.AuthorizationDataModule
import org.timemates.app.authorization.repositories.AuthorizationsRepository
-import org.timemates.app.authorization.ui.confirmation.mvi.ConfirmAuthorizationMiddleware
-import org.timemates.app.authorization.ui.confirmation.mvi.ConfirmAuthorizationStateMachine
-import org.timemates.app.authorization.ui.confirmation.mvi.ConfirmAuthorizationsReducer
+import org.timemates.app.authorization.ui.confirmation.mvi.ConfirmAuthorizationScreenComponent
import org.timemates.app.authorization.usecases.ConfirmEmailAuthorizationUseCase
-import org.timemates.app.authorization.validation.ConfirmationCodeValidator
-import io.timemates.sdk.authorization.email.types.value.VerificationHash
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import org.koin.core.annotation.Factory
-import org.koin.core.annotation.Module
-import org.koin.core.annotation.Singleton
+import org.timemates.sdk.authorization.email.types.value.VerificationHash
@Module(includes = [AuthorizationDataModule::class])
class ConfirmAuthorizationModule {
- @Singleton
- fun confirmationCodeValidator(): ConfirmationCodeValidator = ConfirmationCodeValidator()
-
- @Singleton
- fun middleware(): ConfirmAuthorizationMiddleware = ConfirmAuthorizationMiddleware()
-
@Factory
fun confirmAuthorizationUseCase(
authorizationsRepository: AuthorizationsRepository,
): ConfirmEmailAuthorizationUseCase = ConfirmEmailAuthorizationUseCase(authorizationsRepository)
@Factory
- fun stateMachine(
+ fun mviComponent(
+ componentContext: ComponentContext,
verificationHash: VerificationHash,
- middleware: ConfirmAuthorizationMiddleware,
confirmEmailAuthorizationUseCase: ConfirmEmailAuthorizationUseCase,
- confirmationCodeValidator: ConfirmationCodeValidator,
- ): ConfirmAuthorizationStateMachine {
- return ConfirmAuthorizationStateMachine(
- reducer = ConfirmAuthorizationsReducer(
- verificationHash,
- confirmEmailAuthorizationUseCase,
- confirmationCodeValidator,
- ),
- middleware = middleware,
+ ): ConfirmAuthorizationScreenComponent {
+ return ConfirmAuthorizationScreenComponent(
+ componentContext = componentContext,
+ verificationHash = verificationHash,
+ confirmEmailAuthorizationUseCase = confirmEmailAuthorizationUseCase,
)
}
}
diff --git a/feature/authorization/dependencies/src/commonMain/kotlin/org/timemates/app/authorization/dependencies/screens/InitialAuthorizationModule.kt b/feature/authorization/dependencies/src/commonMain/kotlin/org/timemates/app/authorization/dependencies/screens/InitialAuthorizationModule.kt
index f16936e..101e67c 100644
--- a/feature/authorization/dependencies/src/commonMain/kotlin/org/timemates/app/authorization/dependencies/screens/InitialAuthorizationModule.kt
+++ b/feature/authorization/dependencies/src/commonMain/kotlin/org/timemates/app/authorization/dependencies/screens/InitialAuthorizationModule.kt
@@ -1,16 +1,16 @@
package org.timemates.app.authorization.dependencies.screens
-import org.timemates.app.authorization.ui.initial_authorization.mvi.InitialAuthorizationReducer
-import org.timemates.app.authorization.ui.initial_authorization.mvi.InitialAuthorizationStateMachine
+import com.arkivanov.decompose.ComponentContext
import org.koin.core.annotation.Factory
import org.koin.core.annotation.Module
+import org.timemates.app.authorization.ui.initial_authorization.mvi.InitialAuthorizationComponent
@Module
class InitialAuthorizationModule {
@Factory
- fun stateMachine(): InitialAuthorizationStateMachine {
- return InitialAuthorizationStateMachine(
- reducer = InitialAuthorizationReducer()
+ fun mviComponent(componentContext: ComponentContext): InitialAuthorizationComponent {
+ return InitialAuthorizationComponent(
+ componentContext = componentContext,
)
}
}
\ No newline at end of file
diff --git a/feature/authorization/dependencies/src/commonMain/kotlin/org/timemates/app/authorization/dependencies/screens/NewAccountInfoModule.kt b/feature/authorization/dependencies/src/commonMain/kotlin/org/timemates/app/authorization/dependencies/screens/NewAccountInfoModule.kt
index 32af5d7..14f5037 100644
--- a/feature/authorization/dependencies/src/commonMain/kotlin/org/timemates/app/authorization/dependencies/screens/NewAccountInfoModule.kt
+++ b/feature/authorization/dependencies/src/commonMain/kotlin/org/timemates/app/authorization/dependencies/screens/NewAccountInfoModule.kt
@@ -1,22 +1,22 @@
package org.timemates.app.authorization.dependencies.screens
-import org.timemates.app.authorization.dependencies.AuthorizationDataModule
-import org.timemates.app.authorization.ui.new_account_info.mvi.NewAccountInfoReducer
-import org.timemates.app.authorization.ui.new_account_info.mvi.NewAccountInfoStateMachine
-import io.timemates.sdk.authorization.email.types.value.VerificationHash
+import com.arkivanov.decompose.ComponentContext
import org.koin.core.annotation.Factory
import org.koin.core.annotation.Module
+import org.timemates.app.authorization.dependencies.AuthorizationDataModule
+import org.timemates.app.authorization.ui.new_account_info.mvi.NewAccountInfoScreenComponent
+import org.timemates.sdk.authorization.email.types.value.VerificationHash
@Module(includes = [AuthorizationDataModule::class])
class NewAccountInfoModule {
@Factory
- fun stateMachine(
+ fun mviComponent(
+ componentContext: ComponentContext,
verificationHash: VerificationHash,
- ): NewAccountInfoStateMachine {
- return NewAccountInfoStateMachine(
- reducer = NewAccountInfoReducer(
- verificationHash = verificationHash
- ),
+ ): NewAccountInfoScreenComponent {
+ return NewAccountInfoScreenComponent(
+ componentContext = componentContext,
+ verificationHash = verificationHash,
)
}
}
diff --git a/feature/authorization/dependencies/src/commonMain/kotlin/org/timemates/app/authorization/dependencies/screens/StartAuthorizationModule.kt b/feature/authorization/dependencies/src/commonMain/kotlin/org/timemates/app/authorization/dependencies/screens/StartAuthorizationModule.kt
index c30446c..b8347bd 100644
--- a/feature/authorization/dependencies/src/commonMain/kotlin/org/timemates/app/authorization/dependencies/screens/StartAuthorizationModule.kt
+++ b/feature/authorization/dependencies/src/commonMain/kotlin/org/timemates/app/authorization/dependencies/screens/StartAuthorizationModule.kt
@@ -1,49 +1,28 @@
package org.timemates.app.authorization.dependencies.screens
+import com.arkivanov.decompose.ComponentContext
+import org.koin.core.annotation.Factory
+import org.koin.core.annotation.Module
import org.timemates.app.authorization.dependencies.AuthorizationDataModule
import org.timemates.app.authorization.repositories.AuthorizationsRepository
-import org.timemates.app.authorization.ui.start.mvi.StartAuthorizationMiddleware
-import org.timemates.app.authorization.ui.start.mvi.StartAuthorizationReducer
-import org.timemates.app.authorization.ui.start.mvi.StartAuthorizationStateMachine
+import org.timemates.app.authorization.ui.start.mvi.StartAuthorizationComponent
import org.timemates.app.authorization.usecases.AuthorizeByEmailUseCase
-import org.timemates.app.authorization.validation.EmailAddressValidator
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import org.koin.core.annotation.Factory
-import org.koin.core.annotation.Module
-import org.koin.core.annotation.Singleton
@Module(includes = [AuthorizationDataModule::class])
class StartAuthorizationModule {
- @Singleton
- fun emailValidator(): EmailAddressValidator = EmailAddressValidator()
@Factory
fun authByEmailUseCase(authorizationsRepository: AuthorizationsRepository): AuthorizeByEmailUseCase =
AuthorizeByEmailUseCase(authorizationsRepository)
@Factory
- fun reducer(
- emailAddressValidator: EmailAddressValidator,
+ fun mviComponent(
+ componentContext: ComponentContext,
authorizeByEmailUseCase: AuthorizeByEmailUseCase,
- ): StartAuthorizationReducer {
- return StartAuthorizationReducer(
- validateEmail = emailAddressValidator,
+ ): StartAuthorizationComponent {
+ return StartAuthorizationComponent(
+ componentContext = componentContext,
authorizeByEmail = authorizeByEmailUseCase,
)
}
-
- @Singleton
- fun middleware(): StartAuthorizationMiddleware = StartAuthorizationMiddleware()
-
- @Factory
- fun stateMachine(
- reducer: StartAuthorizationReducer,
- middleware: StartAuthorizationMiddleware,
- ): StartAuthorizationStateMachine {
- return StartAuthorizationStateMachine(
- reducer = reducer,
- middleware = middleware,
- )
- }
}
\ No newline at end of file
diff --git a/feature/authorization/domain/src/commonMain/kotlin/org/timemates/app/authorization/repositories/AuthorizationsRepository.kt b/feature/authorization/domain/src/commonMain/kotlin/org/timemates/app/authorization/repositories/AuthorizationsRepository.kt
index 7d0bf42..0266ae5 100644
--- a/feature/authorization/domain/src/commonMain/kotlin/org/timemates/app/authorization/repositories/AuthorizationsRepository.kt
+++ b/feature/authorization/domain/src/commonMain/kotlin/org/timemates/app/authorization/repositories/AuthorizationsRepository.kt
@@ -1,13 +1,13 @@
package org.timemates.app.authorization.repositories
-import io.timemates.sdk.authorization.email.requests.ConfigureNewAccountRequest
-import io.timemates.sdk.authorization.email.requests.ConfirmAuthorizationRequest
-import io.timemates.sdk.authorization.email.types.value.VerificationHash
-import io.timemates.sdk.authorization.sessions.types.Authorization
-import io.timemates.sdk.authorization.sessions.types.value.ConfirmationCode
-import io.timemates.sdk.users.profile.types.value.EmailAddress
-import io.timemates.sdk.users.profile.types.value.UserDescription
-import io.timemates.sdk.users.profile.types.value.UserName
+import org.timemates.sdk.authorization.email.requests.ConfigureNewAccountRequest
+import org.timemates.sdk.authorization.email.requests.ConfirmAuthorizationRequest
+import org.timemates.sdk.authorization.email.types.value.VerificationHash
+import org.timemates.sdk.authorization.sessions.types.Authorization
+import org.timemates.sdk.authorization.sessions.types.value.ConfirmationCode
+import org.timemates.sdk.users.profile.types.value.EmailAddress
+import org.timemates.sdk.users.profile.types.value.UserDescription
+import org.timemates.sdk.users.profile.types.value.UserName
/**
* Interface defining the contract for an authorization repository.
diff --git a/feature/authorization/domain/src/commonMain/kotlin/org/timemates/app/authorization/usecases/AuthorizeByEmailUseCase.kt b/feature/authorization/domain/src/commonMain/kotlin/org/timemates/app/authorization/usecases/AuthorizeByEmailUseCase.kt
index 377d779..1bc023c 100644
--- a/feature/authorization/domain/src/commonMain/kotlin/org/timemates/app/authorization/usecases/AuthorizeByEmailUseCase.kt
+++ b/feature/authorization/domain/src/commonMain/kotlin/org/timemates/app/authorization/usecases/AuthorizeByEmailUseCase.kt
@@ -1,9 +1,9 @@
package org.timemates.app.authorization.usecases
import org.timemates.app.authorization.repositories.AuthorizationsRepository
-import io.timemates.sdk.authorization.email.types.value.VerificationHash
-import io.timemates.sdk.common.exceptions.TooManyRequestsException
-import io.timemates.sdk.users.profile.types.value.EmailAddress
+import org.timemates.sdk.authorization.email.types.value.VerificationHash
+import org.timemates.sdk.common.exceptions.TooManyRequestsException
+import org.timemates.sdk.users.profile.types.value.EmailAddress
class AuthorizeByEmailUseCase(
private val authorizationsRepository: AuthorizationsRepository,
diff --git a/feature/authorization/domain/src/commonMain/kotlin/org/timemates/app/authorization/usecases/ConfirmEmailAuthorizationUseCase.kt b/feature/authorization/domain/src/commonMain/kotlin/org/timemates/app/authorization/usecases/ConfirmEmailAuthorizationUseCase.kt
index 3aaa9ea..95f3596 100644
--- a/feature/authorization/domain/src/commonMain/kotlin/org/timemates/app/authorization/usecases/ConfirmEmailAuthorizationUseCase.kt
+++ b/feature/authorization/domain/src/commonMain/kotlin/org/timemates/app/authorization/usecases/ConfirmEmailAuthorizationUseCase.kt
@@ -1,11 +1,11 @@
package org.timemates.app.authorization.usecases
import org.timemates.app.authorization.repositories.AuthorizationsRepository
-import io.timemates.sdk.authorization.email.types.value.VerificationHash
-import io.timemates.sdk.authorization.sessions.types.Authorization
-import io.timemates.sdk.authorization.sessions.types.value.ConfirmationCode
-import io.timemates.sdk.common.exceptions.InvalidArgumentException
-import io.timemates.sdk.common.exceptions.TooManyRequestsException
+import org.timemates.sdk.authorization.email.types.value.VerificationHash
+import org.timemates.sdk.authorization.sessions.types.Authorization
+import org.timemates.sdk.authorization.sessions.types.value.ConfirmationCode
+import org.timemates.sdk.common.exceptions.InvalidArgumentException
+import org.timemates.sdk.common.exceptions.TooManyRequestsException
class ConfirmEmailAuthorizationUseCase(
private val authorizations: AuthorizationsRepository,
diff --git a/feature/authorization/domain/src/commonMain/kotlin/org/timemates/app/authorization/usecases/CreateNewAccountUseCase.kt b/feature/authorization/domain/src/commonMain/kotlin/org/timemates/app/authorization/usecases/CreateNewAccountUseCase.kt
index 6f4e976..49ae339 100644
--- a/feature/authorization/domain/src/commonMain/kotlin/org/timemates/app/authorization/usecases/CreateNewAccountUseCase.kt
+++ b/feature/authorization/domain/src/commonMain/kotlin/org/timemates/app/authorization/usecases/CreateNewAccountUseCase.kt
@@ -1,10 +1,10 @@
package org.timemates.app.authorization.usecases
import org.timemates.app.authorization.repositories.AuthorizationsRepository
-import io.timemates.sdk.authorization.email.types.value.VerificationHash
-import io.timemates.sdk.authorization.sessions.types.Authorization
-import io.timemates.sdk.users.profile.types.value.UserDescription
-import io.timemates.sdk.users.profile.types.value.UserName
+import org.timemates.sdk.authorization.email.types.value.VerificationHash
+import org.timemates.sdk.authorization.sessions.types.Authorization
+import org.timemates.sdk.users.profile.types.value.UserDescription
+import org.timemates.sdk.users.profile.types.value.UserName
class CreateNewAccountUseCase(
private val authorizationsRepository: AuthorizationsRepository,
diff --git a/feature/authorization/domain/src/commonMain/kotlin/org/timemates/app/authorization/validation/ConfirmationCodeValidator.kt b/feature/authorization/domain/src/commonMain/kotlin/org/timemates/app/authorization/validation/ConfirmationCodeValidator.kt
deleted file mode 100644
index 42ba2d1..0000000
--- a/feature/authorization/domain/src/commonMain/kotlin/org/timemates/app/authorization/validation/ConfirmationCodeValidator.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-package org.timemates.app.authorization.validation
-
-import org.timemates.app.foundation.validation.Validator
-import org.timemates.app.foundation.validation.unknownValidationFailure
-import io.timemates.sdk.authorization.sessions.types.value.ConfirmationCode
-import io.timemates.sdk.common.constructor.CreationFailure
-
-/**
- * A validator for confirmation codes.
- */
-class ConfirmationCodeValidator : Validator {
- /**
- * Validates the given confirmation code.
- *
- * @param input The confirmation code to be validated.
- * @return The validation result.
- */
- override fun validate(input: String): Result {
- return ConfirmationCode.create(input)
- .map { Result.Success }
- .getOrElse {
- when (it) {
- is CreationFailure.SizeExactFailure, is CreationFailure.BlankValueFailure ->
- Result.SizeIsInvalid
- is CreationFailure.PatternFailure -> Result.PatternFailure
- else -> unknownValidationFailure(it)
- }
- }
- }
-
- /**
- * The possible validation results for a confirmation code.
- */
- sealed class Result {
- object SizeIsInvalid : Result()
-
- object PatternFailure : Result()
-
- object Success : Result()
- }
-}
diff --git a/feature/authorization/domain/src/commonMain/kotlin/org/timemates/app/authorization/validation/EmailAddressValidator.kt b/feature/authorization/domain/src/commonMain/kotlin/org/timemates/app/authorization/validation/EmailAddressValidator.kt
deleted file mode 100644
index 1565b6e..0000000
--- a/feature/authorization/domain/src/commonMain/kotlin/org/timemates/app/authorization/validation/EmailAddressValidator.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-package org.timemates.app.authorization.validation
-
-import org.timemates.app.foundation.validation.Validator
-import org.timemates.app.foundation.validation.unknownValidationFailure
-import io.timemates.sdk.common.constructor.CreationFailure
-import io.timemates.sdk.users.profile.types.value.EmailAddress
-
-/**
- * A validator for email addresses.
- */
-class EmailAddressValidator : Validator {
-
- /**
- * Validates the given email address.
- *
- * @param input The email address to be validated.
- * @return The validation result.
- */
- override fun validate(input: String): Result {
- return EmailAddress.create(input)
- .map { Result.Success }
- .getOrElse {
- when (it) {
- is CreationFailure.SizeRangeFailure -> Result.SizeViolation
- is CreationFailure.PatternFailure -> Result.PatternDoesNotMatch
- else -> unknownValidationFailure(it)
- }
- }
- }
-
- /**
- * The possible validation results for an email address.
- */
- sealed class Result {
- object Success : Result()
-
- object SizeViolation : Result()
-
- object PatternDoesNotMatch : Result()
- }
-}
\ No newline at end of file
diff --git a/feature/authorization/domain/src/commonMain/kotlin/org/timemates/app/authorization/validation/UserDescriptionValidator.kt b/feature/authorization/domain/src/commonMain/kotlin/org/timemates/app/authorization/validation/UserDescriptionValidator.kt
deleted file mode 100644
index 4b5dee6..0000000
--- a/feature/authorization/domain/src/commonMain/kotlin/org/timemates/app/authorization/validation/UserDescriptionValidator.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-package org.timemates.app.authorization.validation
-
-import org.timemates.app.foundation.validation.Validator
-import org.timemates.app.foundation.validation.unknownValidationFailure
-import io.timemates.sdk.common.constructor.CreationFailure
-import io.timemates.sdk.users.profile.types.value.UserDescription
-
-class UserDescriptionValidator : Validator {
- override fun validate(input: String): Result {
- return UserDescription.create(input)
- .map { Result.Success }
- .getOrElse { throwable ->
- when (throwable) {
- is CreationFailure.SizeRangeFailure ->
- Result.SizeViolation
-
- else -> unknownValidationFailure(throwable)
- }
- }
- }
-
- sealed class Result {
- object Success : Result()
-
- object SizeViolation : Result()
- }
-}
\ No newline at end of file
diff --git a/feature/authorization/domain/src/commonMain/kotlin/org/timemates/app/authorization/validation/UserNameValidator.kt b/feature/authorization/domain/src/commonMain/kotlin/org/timemates/app/authorization/validation/UserNameValidator.kt
deleted file mode 100644
index 05d270f..0000000
--- a/feature/authorization/domain/src/commonMain/kotlin/org/timemates/app/authorization/validation/UserNameValidator.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package org.timemates.app.authorization.validation
-
-import org.timemates.app.authorization.validation.UserNameValidator.Result
-import org.timemates.app.foundation.validation.Validator
-import org.timemates.app.foundation.validation.unknownValidationFailure
-import io.timemates.sdk.common.constructor.CreationFailure
-import io.timemates.sdk.users.profile.types.value.UserName
-
-class UserNameValidator : Validator {
- override fun validate(input: String): Result {
- return UserName.create(input)
- .map { Result.Success }
- .getOrElse { throwable ->
- when (throwable) {
- is CreationFailure.SizeRangeFailure ->
- Result.SizeViolation
-
- else -> unknownValidationFailure(throwable)
- }
- }
- }
-
- sealed class Result {
- object Success : Result()
-
- object SizeViolation : Result()
- }
-}
\ No newline at end of file
diff --git a/feature/authorization/domain/src/jvmTest/kotlin/org/timemates/app/authorization/usecases/AuthorizeByEmailUseCaseTest.kt b/feature/authorization/domain/src/jvmTest/kotlin/org/timemates/app/authorization/usecases/AuthorizeByEmailUseCaseTest.kt
index e4c9927..ac66b02 100644
--- a/feature/authorization/domain/src/jvmTest/kotlin/org/timemates/app/authorization/usecases/AuthorizeByEmailUseCaseTest.kt
+++ b/feature/authorization/domain/src/jvmTest/kotlin/org/timemates/app/authorization/usecases/AuthorizeByEmailUseCaseTest.kt
@@ -2,13 +2,13 @@ package org.timemates.app.authorization.usecases
import io.mockk.coEvery
import io.mockk.mockk
+import kotlinx.coroutines.runBlocking
import org.timemates.app.authorization.repositories.AuthorizationsRepository
import org.timemates.app.foundation.random.nextString
-import io.timemates.sdk.authorization.email.types.value.VerificationHash
-import io.timemates.sdk.common.constructor.createOrThrow
-import io.timemates.sdk.common.exceptions.TooManyRequestsException
-import io.timemates.sdk.users.profile.types.value.EmailAddress
-import kotlinx.coroutines.runBlocking
+import org.timemates.sdk.authorization.email.types.value.VerificationHash
+import org.timemates.sdk.common.constructor.createOrThrow
+import org.timemates.sdk.common.exceptions.TooManyRequestsException
+import org.timemates.sdk.users.profile.types.value.EmailAddress
import kotlin.random.Random
import kotlin.test.Test
import kotlin.test.assertEquals
@@ -21,8 +21,8 @@ class AuthorizeByEmailUseCaseTest {
@Test
fun `execute with successful authorization should return Success`() {
// GIVEN
- val emailAddress = EmailAddress.createOrThrow("test@example.com")
- val verificationHash = VerificationHash.createOrThrow(Random.nextString(VerificationHash.SIZE))
+ val emailAddress = EmailAddress.factory.createOrThrow("test@example.com")
+ val verificationHash = VerificationHash.factory.createOrThrow(Random.nextString(VerificationHash.REQUIRED_LENGTH))
coEvery { authorizationsRepository.authorize(emailAddress) } returns Result.success(verificationHash)
// WHEN
@@ -38,7 +38,7 @@ class AuthorizeByEmailUseCaseTest {
@Test
fun `execute with TooManyRequestsException should return TooManyRequests`() {
// GIVEN
- val emailAddress = EmailAddress.createOrThrow("test@example.com")
+ val emailAddress = EmailAddress.factory.createOrThrow("test@example.com")
val exception = TooManyRequestsException("Too many requests", cause = null)
coEvery { authorizationsRepository.authorize(emailAddress) } returns Result.failure(exception)
@@ -55,7 +55,7 @@ class AuthorizeByEmailUseCaseTest {
@Test
fun `execute with other exception should return Failure`() {
// GIVEN
- val emailAddress = EmailAddress.createOrThrow("test@example.com")
+ val emailAddress = EmailAddress.factory.createOrThrow("test@example.com")
val exception = RuntimeException("Something went wrong")
coEvery { authorizationsRepository.authorize(emailAddress) } returns Result.failure(exception)
diff --git a/feature/authorization/domain/src/jvmTest/kotlin/org/timemates/app/authorization/usecases/ConfirmEmailAuthorizationUseCaseTest.kt b/feature/authorization/domain/src/jvmTest/kotlin/org/timemates/app/authorization/usecases/ConfirmEmailAuthorizationUseCaseTest.kt
index f03d974..bb71c74 100644
--- a/feature/authorization/domain/src/jvmTest/kotlin/org/timemates/app/authorization/usecases/ConfirmEmailAuthorizationUseCaseTest.kt
+++ b/feature/authorization/domain/src/jvmTest/kotlin/org/timemates/app/authorization/usecases/ConfirmEmailAuthorizationUseCaseTest.kt
@@ -2,16 +2,16 @@ package org.timemates.app.authorization.usecases
import io.mockk.coEvery
import io.mockk.mockk
+import kotlinx.coroutines.runBlocking
import org.timemates.app.authorization.repositories.AuthorizationsRepository
import org.timemates.app.foundation.random.nextString
-import io.timemates.sdk.authorization.email.requests.ConfirmAuthorizationRequest
-import io.timemates.sdk.authorization.email.types.value.VerificationHash
-import io.timemates.sdk.authorization.sessions.types.Authorization
-import io.timemates.sdk.authorization.sessions.types.value.ConfirmationCode
-import io.timemates.sdk.common.constructor.createOrThrow
-import io.timemates.sdk.common.exceptions.InternalServerError
-import io.timemates.sdk.common.exceptions.TooManyRequestsException
-import kotlinx.coroutines.runBlocking
+import org.timemates.sdk.authorization.email.requests.ConfirmAuthorizationRequest
+import org.timemates.sdk.authorization.email.types.value.VerificationHash
+import org.timemates.sdk.authorization.sessions.types.Authorization
+import org.timemates.sdk.authorization.sessions.types.value.ConfirmationCode
+import org.timemates.sdk.common.constructor.createOrThrow
+import org.timemates.sdk.common.exceptions.InternalServerError
+import org.timemates.sdk.common.exceptions.TooManyRequestsException
import kotlin.random.Random
import kotlin.test.Test
import kotlin.test.assertEquals
@@ -24,8 +24,8 @@ class ConfirmEmailAuthorizationUseCaseTest {
@Test
fun `execute with valid verification hash and confirmation code should return Success result`() {
// GIVEN
- val verificationHash = VerificationHash.createOrThrow(Random.nextString(VerificationHash.SIZE))
- val confirmationCode = ConfirmationCode.createOrThrow("12345678")
+ val verificationHash = VerificationHash.factory.createOrThrow(Random.nextString(VerificationHash.SIZE))
+ val confirmationCode = ConfirmationCode.factory.createOrThrow("12345678")
val authorization: Authorization = mockk()
val isNewAccount = false
coEvery { authorizationsRepository.confirm(verificationHash, confirmationCode) } returns
@@ -45,8 +45,8 @@ class ConfirmEmailAuthorizationUseCaseTest {
@Test
fun `execute with TooManyRequestsException should return AttemptsExceeded result`() {
// GIVEN
- val verificationHash = VerificationHash.createOrThrow(Random.nextString(VerificationHash.SIZE))
- val confirmationCode = ConfirmationCode.createOrThrow("12345678")
+ val verificationHash = VerificationHash.factory.createOrThrow(Random.nextString(VerificationHash.SIZE))
+ val confirmationCode = ConfirmationCode.factory.createOrThrow("12345678")
val exception = TooManyRequestsException("Too many requests", cause = null)
coEvery { authorizationsRepository.confirm(verificationHash, confirmationCode) } returns Result.failure(exception)
@@ -60,8 +60,8 @@ class ConfirmEmailAuthorizationUseCaseTest {
@Test
fun `execute with other exceptions should return Failure result`() {
// GIVEN
- val verificationHash = VerificationHash.createOrThrow(Random.nextString(VerificationHash.SIZE))
- val confirmationCode = ConfirmationCode.createOrThrow("12345678")
+ val verificationHash = VerificationHash.factory.createOrThrow(Random.nextString(VerificationHash.SIZE))
+ val confirmationCode = ConfirmationCode.factory.createOrThrow("12345678")
val exception = InternalServerError("Some error", cause = null)
coEvery { authorizationsRepository.confirm(verificationHash, confirmationCode) } returns Result.failure(exception)
diff --git a/feature/authorization/presentation/build.gradle.kts b/feature/authorization/presentation/build.gradle.kts
index b1fdbe0..798c065 100644
--- a/feature/authorization/presentation/build.gradle.kts
+++ b/feature/authorization/presentation/build.gradle.kts
@@ -1,15 +1,15 @@
plugins {
id(libs.plugins.configurations.compose.multiplatform.get().pluginId)
id(libs.plugins.configurations.unit.tests.mockable.get().pluginId)
+ alias(libs.plugins.kotlinx.serialization)
}
dependencies {
- commonMainImplementation(projects.feature.common.presentation)
+ commonMainImplementation(projects.core.ui)
commonMainImplementation(libs.timemates.sdk)
- commonMainApi(projects.foundation.mvi)
commonMainImplementation(projects.feature.authorization.domain)
- commonMainImplementation(projects.styleSystem)
+ commonMainImplementation(projects.core.styleSystem)
commonTestImplementation(projects.foundation.random)
}
diff --git a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/afterstart/AfterStartScreen.kt b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/afterstart/AfterStartScreen.kt
index 0d4af7f..f37c0c9 100644
--- a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/afterstart/AfterStartScreen.kt
+++ b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/afterstart/AfterStartScreen.kt
@@ -8,48 +8,42 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.ArrowBack
+import androidx.compose.material.icons.automirrored.rounded.ArrowBack
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.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import io.github.skeptick.libres.compose.painterResource
-import org.timemates.app.authorization.ui.afterstart.mvi.AfterStartStateMachine
-import org.timemates.app.authorization.ui.afterstart.mvi.AfterStartStateMachine.Event
-import org.timemates.app.foundation.mvi.EmptyState
-import org.timemates.app.foundation.mvi.StateMachine
+import org.timemates.app.authorization.ui.afterstart.mvi.AfterStartScreenComponent
+import org.timemates.app.authorization.ui.afterstart.mvi.AfterStartScreenComponent.*
+import org.timemates.app.feature.common.MVI
import org.timemates.app.localization.compose.LocalStrings
import org.timemates.app.style.system.Resources
import org.timemates.app.style.system.appbar.AppBar
import org.timemates.app.style.system.button.Button
import org.timemates.app.style.system.theme.AppTheme
-import kotlinx.coroutines.channels.consumeEach
+import pro.respawn.flowmvi.essenty.compose.subscribe
@Composable
fun AfterStartScreen(
- stateMachine: StateMachine,
+ mvi: MVI,
navigateToConfirmation: (String) -> Unit,
navigateToStart: () -> Unit,
) {
val painter: Painter = Resources.image.confirm_authorization_info_image.painterResource()
- LaunchedEffect(Unit) {
- stateMachine.effects.consumeEach { effect ->
- when (effect) {
- is AfterStartStateMachine.Effect.NavigateToConfirmation ->
- navigateToConfirmation(effect.verificationHash.string)
-
- AfterStartStateMachine.Effect.OnChangeEmailClicked ->
- navigateToStart()
- }
+ @Suppress("UNUSED_VARIABLE")
+ val state = mvi.subscribe { action ->
+ when (action) {
+ is Action.NavigateToConfirmation -> navigateToConfirmation(action.verificationHash.string)
+ Action.OnChangeEmailClicked -> navigateToStart()
}
}
@@ -58,9 +52,9 @@ fun AfterStartScreen(
AppBar(
navigationIcon = {
IconButton(
- onClick = { stateMachine.dispatchEvent(Event.OnChangeEmailClicked) },
+ onClick = { mvi.store.intent(Intent.OnChangeEmailClicked) },
) {
- Icon(Icons.Rounded.ArrowBack, contentDescription = null)
+ Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = null)
}
},
title = LocalStrings.current.appName,
@@ -106,7 +100,7 @@ fun AfterStartScreen(
Button(
modifier = Modifier.fillMaxWidth(),
primary = false,
- onClick = { stateMachine.dispatchEvent(Event.OnChangeEmailClicked) },
+ onClick = { mvi.store.intent(Intent.OnChangeEmailClicked) },
) {
Text(LocalStrings.current.changeEmail)
}
@@ -114,7 +108,7 @@ fun AfterStartScreen(
Button(
modifier = Modifier.fillMaxWidth(),
primary = true,
- onClick = { stateMachine.dispatchEvent(Event.NextClicked) },
+ onClick = { mvi.store.intent(Intent.NextClicked) },
) {
Text(LocalStrings.current.nextStep)
}
diff --git a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/afterstart/mvi/AfterStartReducer.kt b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/afterstart/mvi/AfterStartReducer.kt
deleted file mode 100644
index ecb3da2..0000000
--- a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/afterstart/mvi/AfterStartReducer.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package org.timemates.app.authorization.ui.afterstart.mvi
-
-import org.timemates.app.authorization.ui.afterstart.mvi.AfterStartStateMachine.Effect
-import org.timemates.app.authorization.ui.afterstart.mvi.AfterStartStateMachine.Event
-import org.timemates.app.foundation.mvi.EmptyState
-import org.timemates.app.foundation.mvi.Reducer
-import org.timemates.app.foundation.mvi.ReducerScope
-import io.timemates.sdk.authorization.email.types.value.VerificationHash
-
-class AfterStartReducer(private val verificationHash: VerificationHash) : Reducer {
- override fun ReducerScope.reduce(state: EmptyState, event: Event): EmptyState {
- return when (event) {
- is Event.NextClicked -> {
- sendEffect(Effect.NavigateToConfirmation(verificationHash))
- state
- }
-
- is Event.OnChangeEmailClicked -> {
- sendEffect(Effect.OnChangeEmailClicked)
- state
- }
- }
- }
-}
\ No newline at end of file
diff --git a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/afterstart/mvi/AfterStartScreenComponent.kt b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/afterstart/mvi/AfterStartScreenComponent.kt
new file mode 100644
index 0000000..1bb98da
--- /dev/null
+++ b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/afterstart/mvi/AfterStartScreenComponent.kt
@@ -0,0 +1,45 @@
+package org.timemates.app.authorization.ui.afterstart.mvi
+
+import androidx.compose.runtime.Immutable
+import com.arkivanov.decompose.ComponentContext
+import org.timemates.app.authorization.ui.afterstart.mvi.AfterStartScreenComponent.Action
+import org.timemates.app.authorization.ui.afterstart.mvi.AfterStartScreenComponent.Intent
+import org.timemates.app.authorization.ui.afterstart.mvi.AfterStartScreenComponent.State
+import org.timemates.app.feature.common.MVI
+import org.timemates.sdk.authorization.email.types.value.VerificationHash
+import pro.respawn.flowmvi.api.MVIAction
+import pro.respawn.flowmvi.api.MVIIntent
+import pro.respawn.flowmvi.api.MVIState
+import pro.respawn.flowmvi.api.Store
+import pro.respawn.flowmvi.essenty.dsl.retainedStore
+import pro.respawn.flowmvi.plugins.reduce
+
+class AfterStartScreenComponent(
+ componentContext: ComponentContext,
+ private val verificationHash: VerificationHash,
+) : ComponentContext by componentContext, MVI {
+
+ override val store: Store = retainedStore(initial = State) {
+ reduce { intent ->
+ when (intent) {
+ Intent.NextClicked -> action(Action.NavigateToConfirmation(verificationHash))
+ Intent.OnChangeEmailClicked -> action(Action.OnChangeEmailClicked)
+ }
+ }
+ }
+
+ @Immutable
+ data object State : MVIState
+
+ sealed class Action : MVIAction {
+ data class NavigateToConfirmation(val verificationHash: VerificationHash) : Action()
+
+ data object OnChangeEmailClicked : Action()
+ }
+
+ sealed class Intent : MVIIntent {
+ data object NextClicked : Intent()
+
+ data object OnChangeEmailClicked : Intent()
+ }
+}
\ No newline at end of file
diff --git a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/afterstart/mvi/AfterStartStateMachine.kt b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/afterstart/mvi/AfterStartStateMachine.kt
deleted file mode 100644
index b8cc69a..0000000
--- a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/afterstart/mvi/AfterStartStateMachine.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-package org.timemates.app.authorization.ui.afterstart.mvi
-
-import org.timemates.app.authorization.ui.afterstart.mvi.AfterStartStateMachine.Effect
-import org.timemates.app.authorization.ui.afterstart.mvi.AfterStartStateMachine.Event
-import org.timemates.app.foundation.mvi.EmptyState
-import org.timemates.app.foundation.mvi.StateMachine
-import org.timemates.app.foundation.mvi.UiEffect
-import org.timemates.app.foundation.mvi.UiEvent
-import io.timemates.sdk.authorization.email.types.value.VerificationHash
-
-class AfterStartStateMachine(
- reducer: AfterStartReducer,
-) : StateMachine(
- initState = EmptyState,
- reducer = reducer,
- middlewares = emptyList(),
-) {
- sealed class Effect : UiEffect {
- data class NavigateToConfirmation(val verificationHash: VerificationHash) : Effect()
-
- object OnChangeEmailClicked : Effect()
- }
-
- sealed class Event : UiEvent {
- object NextClicked : Event()
-
- object OnChangeEmailClicked : Event()
- }
-}
\ No newline at end of file
diff --git a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/configure_account/ConfigureAccountScreen.kt b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/configure_account/ConfigureAccountScreen.kt
index d09abdf..4ab2e07 100644
--- a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/configure_account/ConfigureAccountScreen.kt
+++ b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/configure_account/ConfigureAccountScreen.kt
@@ -8,8 +8,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.outlined.PersonOutline
-import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
@@ -19,52 +19,47 @@ import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
-import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountStateMachine.Effect
-import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountStateMachine.Event
-import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountStateMachine.State
+import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountScreenComponent
+import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountScreenComponent.*
+import org.timemates.app.feature.common.MVI
import org.timemates.app.feature.common.failures.getDefaultDisplayMessage
-import org.timemates.app.foundation.mvi.StateMachine
+import org.timemates.app.feature.common.getFailuresIfPresent
+import org.timemates.app.feature.common.isInvalid
import org.timemates.app.localization.compose.LocalStrings
import org.timemates.app.style.system.appbar.AppBar
import org.timemates.app.style.system.button.ButtonWithProgress
-import io.timemates.sdk.users.profile.types.value.UserDescription
-import io.timemates.sdk.users.profile.types.value.UserName
-import kotlinx.coroutines.channels.consumeEach
+import org.timemates.sdk.users.profile.types.value.UserDescription
+import org.timemates.sdk.users.profile.types.value.UserName
+import pro.respawn.flowmvi.essenty.compose.subscribe
@Composable
fun ConfigureAccountScreen(
- stateMachine: StateMachine,
+ mvi: MVI,
navigateToHome: () -> Unit,
onBack: () -> Unit,
) {
- val state by stateMachine.state.collectAsState()
- val snackbarData = remember { SnackbarHostState() }
-
- val nameSize = remember(state.name) { state.name.length }
- val aboutYouSize = remember(state.aboutYou) { state.aboutYou.length }
-
+ val snackBarData = remember { SnackbarHostState() }
val strings = LocalStrings.current
- LaunchedEffect(Unit) {
- stateMachine.effects.consumeEach { effect ->
- when (effect) {
- is Effect.Failure ->
- snackbarData.showSnackbar(message = effect.throwable.getDefaultDisplayMessage(strings))
+ val state by mvi.subscribe { action ->
+ when (action) {
+ is Action.ShowFailure ->
+ snackBarData.showSnackbar(message = action.throwable.getDefaultDisplayMessage(strings))
- is Effect.NavigateToHomePage -> navigateToHome()
- Effect.NavigateToStart -> onBack()
- }
+ is Action.NavigateToHomePage -> navigateToHome()
+ Action.NavigateToStart -> onBack()
}
}
+ val nameSize = remember(state.name) { state.name.value.length }
+ val aboutYouSize = remember(state.aboutYou) { state.aboutYou.value.length }
+
Scaffold(
topBar = {
AppBar(
@@ -72,21 +67,15 @@ fun ConfigureAccountScreen(
IconButton(
onClick = onBack,
) {
- Icon(Icons.Rounded.ArrowBack, contentDescription = null)
+ Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = null)
}
},
title = LocalStrings.current.confirmation,
)
},
) { rootPaddings ->
- val nameSupportText = when {
- state.isNameSizeInvalid -> LocalStrings.current.nameSizeIsInvalid
- else -> null
- }
- val aboutYouSupportText = when {
- state.isAboutYouSizeInvalid -> LocalStrings.current.aboutYouSizeIsInvalid
- else -> null
- }
+ val nameSupportText = state.name.getFailuresIfPresent(strings)
+ val aboutYouSupportText = state.aboutYou.getFailuresIfPresent(strings)
Column(
modifier = Modifier.fillMaxHeight().padding(rootPaddings).padding(16.dp),
@@ -95,10 +84,10 @@ fun ConfigureAccountScreen(
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
leadingIcon = { Icon(Icons.Outlined.PersonOutline, contentDescription = null) },
- value = state.name,
- onValueChange = { stateMachine.dispatchEvent(Event.NameIsChanged(it)) },
+ value = state.name.value,
+ onValueChange = { mvi.store.intent(Intent.NameIsChanged(it)) },
label = { Text(LocalStrings.current.yourName) },
- isError = state.isNameSizeInvalid || nameSize > UserName.SIZE_RANGE.last,
+ isError = state.name.isInvalid() && nameSize > UserName.LENGTH_RANGE.last,
singleLine = true,
supportingText = {
if (nameSupportText != null) {
@@ -106,7 +95,7 @@ fun ConfigureAccountScreen(
} else {
Text(
modifier = Modifier.fillMaxWidth(),
- text = "$nameSize / ${UserName.SIZE_RANGE.last}",
+ text = "$nameSize / ${UserName.LENGTH_RANGE.last}",
textAlign = TextAlign.End
)
}
@@ -117,10 +106,10 @@ fun ConfigureAccountScreen(
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
- value = state.aboutYou,
- onValueChange = { stateMachine.dispatchEvent(Event.DescriptionIsChanged(it)) },
+ value = state.aboutYou.value,
+ onValueChange = { mvi.store.intent(Intent.DescriptionIsChanged(it)) },
label = { Text(LocalStrings.current.aboutYou) },
- isError = state.isAboutYouSizeInvalid || aboutYouSize > UserDescription.SIZE_RANGE.last,
+ isError = state.aboutYou.isInvalid() || aboutYouSize > UserDescription.LENGTH_RANGE.last,
maxLines = 5,
supportingText = {
if (aboutYouSupportText != null) {
@@ -128,7 +117,7 @@ fun ConfigureAccountScreen(
} else {
Text(
modifier = Modifier.fillMaxWidth(),
- text = "$aboutYouSize / ${UserDescription.SIZE_RANGE.last}",
+ text = "$aboutYouSize / ${UserDescription.LENGTH_RANGE.last}",
textAlign = TextAlign.End
)
}
@@ -143,14 +132,14 @@ fun ConfigureAccountScreen(
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
SnackbarHost(
- hostState = snackbarData,
+ hostState = snackBarData,
) {
Snackbar(it)
}
ButtonWithProgress(
primary = true,
modifier = Modifier.fillMaxWidth(),
- onClick = { stateMachine.dispatchEvent(Event.OnDoneClicked) },
+ onClick = { mvi.store.intent(Intent.OnDoneClicked) },
enabled = !state.isLoading,
isLoading = state.isLoading
) {
diff --git a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/configure_account/mvi/ConfigureAccountMiddleware.kt b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/configure_account/mvi/ConfigureAccountMiddleware.kt
deleted file mode 100644
index e2fa1df..0000000
--- a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/configure_account/mvi/ConfigureAccountMiddleware.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package org.timemates.app.authorization.ui.configure_account.mvi
-
-import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountStateMachine.Effect
-import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountStateMachine.State
-import org.timemates.app.foundation.mvi.Middleware
-import org.timemates.app.foundation.mvi.StateStore
-
-class ConfigureAccountMiddleware : Middleware {
- override fun onEffect(effect: Effect, state: State): State {
- return when (effect) {
- is Effect.Failure ->
- state.copy(isLoading = false)
-
- else -> state
- }
- }
-}
diff --git a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/configure_account/mvi/ConfigureAccountReducer.kt b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/configure_account/mvi/ConfigureAccountReducer.kt
deleted file mode 100644
index bc6f156..0000000
--- a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/configure_account/mvi/ConfigureAccountReducer.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-package org.timemates.app.authorization.ui.configure_account.mvi
-
-import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountStateMachine.Effect
-import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountStateMachine.Event
-import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountStateMachine.State
-import org.timemates.app.authorization.usecases.CreateNewAccountUseCase
-import org.timemates.app.authorization.validation.UserDescriptionValidator
-import org.timemates.app.authorization.validation.UserNameValidator
-import org.timemates.app.foundation.mvi.Reducer
-import org.timemates.app.foundation.mvi.ReducerScope
-import io.timemates.sdk.authorization.email.types.value.VerificationHash
-import io.timemates.sdk.common.constructor.createOrThrow
-import io.timemates.sdk.users.profile.types.value.UserDescription
-import io.timemates.sdk.users.profile.types.value.UserName
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
-
-class ConfigureAccountReducer(
- private val verificationHash: VerificationHash,
- private val createNewAccountUseCase: CreateNewAccountUseCase,
- private val userNameValidator: UserNameValidator,
- private val userDescriptionValidator: UserDescriptionValidator,
-) : Reducer {
- override fun ReducerScope.reduce(state: State, event: Event): State {
- return when (event) {
- Event.OnDoneClicked -> {
- val name = when (userNameValidator.validate(state.name)) {
- is UserNameValidator.Result.SizeViolation ->
- return state.copy(isNameSizeInvalid = true)
-
- else -> UserName.createOrThrow(state.name)
- }
- val description = when (userDescriptionValidator.validate(state.aboutYou)) {
- is UserDescriptionValidator.Result.SizeViolation ->
- return state.copy(isAboutYouSizeInvalid = true)
-
- else -> UserDescription.createOrThrow(state.aboutYou)
- }
-
- completeRegistration(name, description, sendEffect, machineScope)
- return state.copy(isLoading = true)
- }
-
- is Event.NameIsChanged ->
- state.copy(name = event.name, isNameSizeInvalid = false)
-
- is Event.DescriptionIsChanged ->
- state.copy(aboutYou = event.description, isAboutYouSizeInvalid = false)
- }
- }
-
- private fun completeRegistration(
- name: UserName,
- description: UserDescription,
- sendEffect: (Effect) -> Unit,
- scope: CoroutineScope,
- ) {
- scope.launch {
- when (val result = createNewAccountUseCase.execute(verificationHash, name, description)) {
- is CreateNewAccountUseCase.Result.Failure ->
- sendEffect(Effect.Failure(result.exception))
-
- is CreateNewAccountUseCase.Result.Success ->
- sendEffect(Effect.NavigateToHomePage(result.authorization))
- }
- }
- }
-}
\ No newline at end of file
diff --git a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/configure_account/mvi/ConfigureAccountScreenComponent.kt b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/configure_account/mvi/ConfigureAccountScreenComponent.kt
new file mode 100644
index 0000000..c1498a4
--- /dev/null
+++ b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/configure_account/mvi/ConfigureAccountScreenComponent.kt
@@ -0,0 +1,105 @@
+package org.timemates.app.authorization.ui.configure_account.mvi
+
+import androidx.compose.runtime.Immutable
+import com.arkivanov.decompose.ComponentContext
+import kotlinx.coroutines.launch
+import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountScreenComponent.Action
+import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountScreenComponent.Intent
+import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountScreenComponent.State
+import org.timemates.app.authorization.usecases.CreateNewAccountUseCase
+import org.timemates.app.feature.common.Input
+import org.timemates.app.feature.common.MVI
+import org.timemates.app.feature.common.input
+import org.timemates.app.feature.common.isValid
+import org.timemates.sdk.authorization.email.types.value.VerificationHash
+import org.timemates.sdk.authorization.sessions.types.Authorization
+import org.timemates.sdk.common.constructor.createOrThrow
+import org.timemates.sdk.users.profile.types.value.UserDescription
+import org.timemates.sdk.users.profile.types.value.UserName
+import pro.respawn.flowmvi.api.Container
+import pro.respawn.flowmvi.api.MVIAction
+import pro.respawn.flowmvi.api.MVIIntent
+import pro.respawn.flowmvi.api.MVIState
+import pro.respawn.flowmvi.api.PipelineContext
+import pro.respawn.flowmvi.api.Store
+import pro.respawn.flowmvi.essenty.dsl.retainedStore
+import pro.respawn.flowmvi.plugins.recover
+import pro.respawn.flowmvi.plugins.reduce
+
+class ConfigureAccountScreenComponent(
+ componentContext: ComponentContext,
+ private val verificationHash: VerificationHash,
+ private val createNewAccountUseCase: CreateNewAccountUseCase,
+) : ComponentContext by componentContext, MVI {
+
+ override val store: Store = retainedStore(initial = State()) {
+ recover { exception ->
+ action(Action.ShowFailure(exception))
+ null
+ }
+
+ reduce { intent ->
+ updateState {
+ when (intent) {
+ is Intent.DescriptionIsChanged -> copy(aboutYou = input(intent.description))
+ is Intent.NameIsChanged -> copy(name = input(intent.name))
+ Intent.OnDoneClicked -> copy(
+ name = name.validated(UserName.factory),
+ aboutYou = aboutYou.validated(UserDescription.factory),
+ ).run {
+ if (name.isValid() && aboutYou.isValid()) {
+ registerUserAsync(
+ name = UserName.factory.createOrThrow(name.value),
+ description = UserDescription.factory.createOrThrow(aboutYou.value),
+ )
+ copy(isLoading = true)
+ } else this
+ }
+ }
+ }
+ }
+ }
+
+ private fun PipelineContext.registerUserAsync(
+ name: UserName,
+ description: UserDescription,
+ ) {
+ launch {
+ when (val result = createNewAccountUseCase.execute(verificationHash, name, description)) {
+ is CreateNewAccountUseCase.Result.Failure -> {
+ action(Action.ShowFailure(result.exception))
+ }
+
+ is CreateNewAccountUseCase.Result.Success ->
+ action(Action.NavigateToHomePage(result.authorization))
+ }
+
+ updateState { copy(isLoading = false) }
+ }
+ }
+
+ @Immutable
+ data class State(
+ val name: Input = input(""),
+ val aboutYou: Input = input(""),
+ val isLoading: Boolean = false,
+ ) : MVIState
+
+ sealed class Intent : MVIIntent {
+ data class NameIsChanged(val name: String) : Intent()
+
+ data class DescriptionIsChanged(val description: String) : Intent()
+
+ data object OnDoneClicked : Intent()
+ }
+
+ sealed class Action : MVIAction {
+ data class ShowFailure(val throwable: Throwable) : Action()
+
+ data object NavigateToStart : Action()
+
+ data class NavigateToHomePage(
+ val authorization: Authorization,
+ ) : Action()
+ }
+}
\ No newline at end of file
diff --git a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/configure_account/mvi/ConfigureAccountStateMachine.kt b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/configure_account/mvi/ConfigureAccountStateMachine.kt
deleted file mode 100644
index 01f9b95..0000000
--- a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/configure_account/mvi/ConfigureAccountStateMachine.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-package org.timemates.app.authorization.ui.configure_account.mvi
-
-import androidx.compose.runtime.Immutable
-import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountStateMachine.Effect
-import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountStateMachine.Event
-import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountStateMachine.State
-import org.timemates.app.foundation.mvi.StateMachine
-import org.timemates.app.foundation.mvi.UiEffect
-import org.timemates.app.foundation.mvi.UiEvent
-import org.timemates.app.foundation.mvi.UiState
-import io.timemates.sdk.authorization.sessions.types.Authorization
-
-class ConfigureAccountStateMachine(
- reducer: ConfigureAccountReducer,
- middleware: ConfigureAccountMiddleware,
-) : StateMachine(
- initState = State(),
- reducer = reducer,
- middlewares = listOf(middleware),
-) {
- @Immutable
- data class State(
- val name: String = "",
- val aboutYou: String = "",
- val isNameSizeInvalid: Boolean = false,
- val isAboutYouSizeInvalid: Boolean = false,
- val isLoading: Boolean = false,
- ) : UiState
-
- sealed class Event : UiEvent {
- data class NameIsChanged(val name: String) : Event()
-
- data class DescriptionIsChanged(val description: String) : Event()
-
- object OnDoneClicked : Event()
- }
-
- sealed class Effect : UiEffect {
- data class Failure(val throwable: Throwable) : Effect()
-
- object NavigateToStart : Effect()
-
- data class NavigateToHomePage(
- val authorization: Authorization,
- ) : Effect()
- }
-}
\ No newline at end of file
diff --git a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/confirmation/ConfirmAuthorizationScreen.kt b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/confirmation/ConfirmAuthorizationScreen.kt
index edac29f..04eaba2 100644
--- a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/confirmation/ConfirmAuthorizationScreen.kt
+++ b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/confirmation/ConfirmAuthorizationScreen.kt
@@ -18,52 +18,46 @@ import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
-import org.timemates.app.authorization.ui.confirmation.mvi.ConfirmAuthorizationStateMachine.Effect
-import org.timemates.app.authorization.ui.confirmation.mvi.ConfirmAuthorizationStateMachine.Event
-import org.timemates.app.authorization.ui.confirmation.mvi.ConfirmAuthorizationStateMachine.State
+import org.timemates.app.authorization.ui.confirmation.mvi.ConfirmAuthorizationScreenComponent.Action
+import org.timemates.app.authorization.ui.confirmation.mvi.ConfirmAuthorizationScreenComponent.Intent
+import org.timemates.app.authorization.ui.confirmation.mvi.ConfirmAuthorizationScreenComponent.State
+import org.timemates.app.feature.common.MVI
import org.timemates.app.feature.common.failures.getDefaultDisplayMessage
-import org.timemates.app.foundation.mvi.StateMachine
+import org.timemates.app.feature.common.getFailuresIfPresent
import org.timemates.app.localization.compose.LocalStrings
import org.timemates.app.style.system.appbar.AppBar
import org.timemates.app.style.system.button.ButtonWithProgress
-import kotlinx.coroutines.channels.consumeEach
+import pro.respawn.flowmvi.essenty.compose.subscribe
@Composable
fun ConfirmAuthorizationScreen(
- stateMachine: StateMachine,
+ mvi: MVI,
onBack: () -> Unit,
navigateToConfiguring: (String) -> Unit,
navigateToHome: () -> Unit,
) {
- val state by stateMachine.state.collectAsState()
- val snackbarData = remember { SnackbarHostState() }
-
val strings = LocalStrings.current
+ val snackbarData = remember { SnackbarHostState() }
+ val state by mvi.subscribe { action ->
+ when (action) {
+ is Action.Failure ->
+ snackbarData.showSnackbar(message = action.throwable.getDefaultDisplayMessage(strings))
- LaunchedEffect(Unit) {
- stateMachine.effects.consumeEach { effect ->
- when (effect) {
- is Effect.Failure ->
- snackbarData.showSnackbar(message = effect.throwable.getDefaultDisplayMessage(strings))
-
- is Effect.NavigateToCreateAccount ->
- navigateToConfiguring(effect.verificationHash.string)
+ is Action.NavigateToCreateAccount ->
+ navigateToConfiguring(action.verificationHash.string)
- is Effect.NavigateToHome ->
- navigateToHome()
+ is Action.NavigateToHome ->
+ navigateToHome()
- Effect.TooManyAttempts ->
- snackbarData.showSnackbar(message = strings.tooManyAttempts)
+ Action.TooManyAttempts ->
+ snackbarData.showSnackbar(message = strings.tooManyAttempts)
- Effect.AttemptIsFailed ->
- snackbarData.showSnackbar(message = strings.confirmationAttemptFailed)
- }
+ Action.AttemptIsFailed ->
+ snackbarData.showSnackbar(message = strings.confirmationAttemptFailed)
}
}
@@ -81,20 +75,16 @@ fun ConfirmAuthorizationScreen(
)
},
) { rootPaddings ->
- val supportText = when {
- state.isCodeInvalid -> LocalStrings.current.codeIsInvalid
- state.isCodeSizeInvalid -> LocalStrings.current.codeSizeIsInvalid
- else -> null
- }
+ val supportText = state.code.getFailuresIfPresent(strings)
Column(modifier = Modifier.fillMaxHeight().padding(rootPaddings).padding(16.dp)) {
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
leadingIcon = { Icon(Icons.Outlined.Lock, contentDescription = null) },
- value = state.code,
- onValueChange = { stateMachine.dispatchEvent(Event.CodeChange(it)) },
+ value = state.code.value,
+ onValueChange = { mvi.store.intent(Intent.CodeChange(it)) },
label = { Text(LocalStrings.current.confirmation) },
- isError = state.isCodeInvalid || state.isCodeSizeInvalid,
+ isError = supportText != null,
supportingText = { if (supportText != null) Text(supportText) },
enabled = !state.isLoading,
)
@@ -114,8 +104,8 @@ fun ConfirmAuthorizationScreen(
ButtonWithProgress(
primary = true,
modifier = Modifier.fillMaxWidth(),
- onClick = { stateMachine.dispatchEvent(Event.OnConfirmClicked) },
- enabled = !state.isLoading,
+ onClick = { mvi.store.intent(Intent.OnConfirmClicked) },
+ enabled = !state.isLoading && state.canSendRequest,
isLoading = state.isLoading
) {
Text(text = LocalStrings.current.nextStep)
@@ -123,4 +113,4 @@ fun ConfirmAuthorizationScreen(
}
}
}
-}
\ No newline at end of file
+}
diff --git a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/confirmation/mvi/ConfirmAuthorizationMiddleware.kt b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/confirmation/mvi/ConfirmAuthorizationMiddleware.kt
deleted file mode 100644
index f3d68f2..0000000
--- a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/confirmation/mvi/ConfirmAuthorizationMiddleware.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package org.timemates.app.authorization.ui.confirmation.mvi
-
-import org.timemates.app.authorization.ui.confirmation.mvi.ConfirmAuthorizationStateMachine.Effect
-import org.timemates.app.authorization.ui.confirmation.mvi.ConfirmAuthorizationStateMachine.State
-import org.timemates.app.foundation.mvi.Middleware
-import org.timemates.app.foundation.mvi.StateStore
-
-/**
- * This middleware works with async operation effects and updates the state when necessary.
- * Specifically, when an [Effect.Failure] or [Effect.TooManyAttempts] is received, it removes
- * the loading state from the UI.
- */
-class ConfirmAuthorizationMiddleware : Middleware {
- override fun onEffect(effect: Effect, state: State): State {
- return when (effect) {
- is Effect.Failure,
- Effect.TooManyAttempts,
- Effect.AttemptIsFailed,
- is Effect.NavigateToCreateAccount,
- is Effect.NavigateToHome,
- -> state.copy(isLoading = false)
- }
- }
-}
diff --git a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/confirmation/mvi/ConfirmAuthorizationScreenComponent.kt b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/confirmation/mvi/ConfirmAuthorizationScreenComponent.kt
new file mode 100644
index 0000000..09603bb
--- /dev/null
+++ b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/confirmation/mvi/ConfirmAuthorizationScreenComponent.kt
@@ -0,0 +1,116 @@
+package org.timemates.app.authorization.ui.confirmation.mvi
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable
+import com.arkivanov.decompose.ComponentContext
+import kotlinx.coroutines.launch
+import org.timemates.app.authorization.ui.confirmation.mvi.ConfirmAuthorizationScreenComponent.Action
+import org.timemates.app.authorization.ui.confirmation.mvi.ConfirmAuthorizationScreenComponent.Intent
+import org.timemates.app.authorization.ui.confirmation.mvi.ConfirmAuthorizationScreenComponent.State
+import org.timemates.app.authorization.usecases.ConfirmEmailAuthorizationUseCase
+import org.timemates.app.feature.common.Input
+import org.timemates.app.feature.common.MVI
+import org.timemates.app.feature.common.input
+import org.timemates.app.feature.common.isValid
+import org.timemates.sdk.authorization.email.types.value.VerificationHash
+import org.timemates.sdk.authorization.sessions.types.Authorization
+import org.timemates.sdk.authorization.sessions.types.value.ConfirmationCode
+import org.timemates.sdk.common.constructor.createOrThrow
+import pro.respawn.flowmvi.api.Container
+import pro.respawn.flowmvi.api.MVIAction
+import pro.respawn.flowmvi.api.MVIIntent
+import pro.respawn.flowmvi.api.MVIState
+import pro.respawn.flowmvi.api.PipelineContext
+import pro.respawn.flowmvi.api.Store
+import pro.respawn.flowmvi.dsl.emit
+import pro.respawn.flowmvi.essenty.dsl.retainedStore
+import pro.respawn.flowmvi.plugins.recover
+import pro.respawn.flowmvi.plugins.reduce
+
+class ConfirmAuthorizationScreenComponent(
+ componentContext: ComponentContext,
+ private val verificationHash: VerificationHash,
+ private val confirmEmailAuthorizationUseCase: ConfirmEmailAuthorizationUseCase,
+) : ComponentContext by componentContext, MVI {
+
+ override val store: Store = retainedStore(initial = State()) {
+ recover { exception ->
+ emit(Action.Failure(exception))
+ null
+ }
+
+ reduce { intent ->
+ when (intent) {
+ is Intent.CodeChange -> updateState { copy(code = input(intent.code)) }
+ Intent.OnConfirmClicked -> {
+ updateState {
+ copy(code = code.validated(ConfirmationCode.factory)).run {
+ when {
+ code.isValid() -> {
+ sendCodeAsync(ConfirmationCode.factory.createOrThrow(code.value))
+ copy(isLoading = true)
+ }
+
+ else -> this
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun PipelineContext.sendCodeAsync(code: ConfirmationCode) {
+ launch {
+ when (val result = confirmEmailAuthorizationUseCase.execute(verificationHash, code)) {
+ ConfirmEmailAuthorizationUseCase.Result.AttemptsExceeded -> {
+ action(Action.TooManyAttempts)
+ updateState { copy(canSendRequest = false) }
+ }
+
+ is ConfirmEmailAuthorizationUseCase.Result.Failure ->
+ emit(Action.Failure(result.exception))
+
+ ConfirmEmailAuthorizationUseCase.Result.InvalidCode ->
+ emit(Action.AttemptIsFailed)
+
+ is ConfirmEmailAuthorizationUseCase.Result.Success -> {
+ action(
+ if (result.isNewAccount)
+ Action.NavigateToCreateAccount(verificationHash)
+ else Action.NavigateToHome(result.authorization!!)
+ )
+ }
+ }
+ }
+ }
+
+ @Immutable
+ data class State(
+ val code: Input = input(""),
+ val isLoading: Boolean = false,
+ val canSendRequest: Boolean = true,
+ ) : MVIState
+
+ sealed class Intent : MVIIntent {
+ data class CodeChange(val code: String) : Intent()
+
+ data object OnConfirmClicked : Intent()
+ }
+
+ sealed class Action : MVIAction {
+ data object TooManyAttempts : Action()
+
+ data object AttemptIsFailed : Action()
+
+ data class Failure(val throwable: Throwable) : Action()
+
+ data class NavigateToCreateAccount(
+ val verificationHash: VerificationHash,
+ ) : Action()
+
+ data class NavigateToHome(
+ val authorization: Authorization,
+ ) : Action()
+ }
+}
\ No newline at end of file
diff --git a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/confirmation/mvi/ConfirmAuthorizationStateMachine.kt b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/confirmation/mvi/ConfirmAuthorizationStateMachine.kt
deleted file mode 100644
index 4fb42e4..0000000
--- a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/confirmation/mvi/ConfirmAuthorizationStateMachine.kt
+++ /dev/null
@@ -1,51 +0,0 @@
-package org.timemates.app.authorization.ui.confirmation.mvi
-
-import androidx.compose.runtime.Immutable
-import org.timemates.app.authorization.ui.confirmation.mvi.ConfirmAuthorizationStateMachine.Effect
-import org.timemates.app.authorization.ui.confirmation.mvi.ConfirmAuthorizationStateMachine.Event
-import org.timemates.app.authorization.ui.confirmation.mvi.ConfirmAuthorizationStateMachine.State
-import org.timemates.app.foundation.mvi.StateMachine
-import org.timemates.app.foundation.mvi.UiEffect
-import org.timemates.app.foundation.mvi.UiEvent
-import org.timemates.app.foundation.mvi.UiState
-import io.timemates.sdk.authorization.email.types.value.VerificationHash
-import io.timemates.sdk.authorization.sessions.types.Authorization
-
-class ConfirmAuthorizationStateMachine(
- reducer: ConfirmAuthorizationsReducer,
- middleware: ConfirmAuthorizationMiddleware,
-) : StateMachine(
- initState = State(),
- reducer = reducer,
- middlewares = listOf(middleware),
-) {
- @Immutable
- data class State(
- val code: String = "",
- val isCodeInvalid: Boolean = false,
- val isCodeSizeInvalid: Boolean = false,
- val isLoading: Boolean = false,
- ) : UiState
-
- sealed class Event : UiEvent {
- data class CodeChange(val code: String) : Event()
-
- data object OnConfirmClicked : Event()
- }
-
- sealed class Effect : UiEffect {
- data object TooManyAttempts : Effect()
-
- data object AttemptIsFailed : Effect()
-
- data class Failure(val throwable: Throwable) : Effect()
-
- data class NavigateToCreateAccount(
- val verificationHash: VerificationHash,
- ) : Effect()
-
- data class NavigateToHome(
- val authorization: Authorization,
- ) : Effect()
- }
-}
\ No newline at end of file
diff --git a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/confirmation/mvi/ConfirmAuthorizationsReducer.kt b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/confirmation/mvi/ConfirmAuthorizationsReducer.kt
deleted file mode 100644
index 9f7f765..0000000
--- a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/confirmation/mvi/ConfirmAuthorizationsReducer.kt
+++ /dev/null
@@ -1,73 +0,0 @@
-package org.timemates.app.authorization.ui.confirmation.mvi
-
-import org.timemates.app.authorization.ui.confirmation.mvi.ConfirmAuthorizationStateMachine.Effect
-import org.timemates.app.authorization.ui.confirmation.mvi.ConfirmAuthorizationStateMachine.Event
-import org.timemates.app.authorization.ui.confirmation.mvi.ConfirmAuthorizationStateMachine.State
-import org.timemates.app.authorization.usecases.ConfirmEmailAuthorizationUseCase
-import org.timemates.app.authorization.validation.ConfirmationCodeValidator
-import org.timemates.app.foundation.mvi.Reducer
-import org.timemates.app.foundation.mvi.ReducerScope
-import io.timemates.sdk.authorization.email.types.value.VerificationHash
-import io.timemates.sdk.authorization.sessions.types.value.ConfirmationCode
-import io.timemates.sdk.common.constructor.createOrThrow
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
-
-class ConfirmAuthorizationsReducer(
- private val verificationHash: VerificationHash,
- private val confirmEmailAuthorizationUseCase: ConfirmEmailAuthorizationUseCase,
- private val confirmationCodeValidator: ConfirmationCodeValidator,
-) : Reducer {
- override fun ReducerScope.reduce(state: State, event: Event): State {
- return when (event) {
- Event.OnConfirmClicked -> {
- when (confirmationCodeValidator.validate(state.code)) {
- ConfirmationCodeValidator.Result.SizeIsInvalid -> state.copy(
- isCodeSizeInvalid = true,
- isCodeInvalid = false,
- )
-
- ConfirmationCodeValidator.Result.PatternFailure -> state.copy(
- isCodeSizeInvalid = false,
- isCodeInvalid = true,
- )
-
- ConfirmationCodeValidator.Result.Success -> {
- confirm(machineScope, ConfirmationCode.createOrThrow(state.code), sendEffect)
- state.copy(isLoading = true)
- }
- }
- }
-
- is Event.CodeChange -> state.copy(code = event.code)
- Event.OnConfirmClicked -> state
- }
- }
-
- private fun confirm(
- scope: CoroutineScope,
- code: ConfirmationCode,
- sendEffect: (Effect) -> Unit,
- ) {
- scope.launch {
- when (val result = confirmEmailAuthorizationUseCase.execute(verificationHash, code)) {
- ConfirmEmailAuthorizationUseCase.Result.AttemptsExceeded ->
- sendEffect(Effect.TooManyAttempts)
-
- is ConfirmEmailAuthorizationUseCase.Result.Failure ->
- sendEffect(Effect.Failure(result.exception))
-
- ConfirmEmailAuthorizationUseCase.Result.InvalidCode ->
- sendEffect(Effect.AttemptIsFailed)
-
- is ConfirmEmailAuthorizationUseCase.Result.Success -> {
- if (result.isNewAccount) {
- sendEffect(Effect.NavigateToCreateAccount(verificationHash))
- } else {
- sendEffect(Effect.NavigateToHome(result.authorization!!))
- }
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/initial_authorization/InitialAuthorizationScreen.kt b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/initial_authorization/InitialAuthorizationScreen.kt
index 14b92a8..23244a7 100644
--- a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/initial_authorization/InitialAuthorizationScreen.kt
+++ b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/initial_authorization/InitialAuthorizationScreen.kt
@@ -12,36 +12,35 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import io.github.skeptick.libres.compose.painterResource
-import org.timemates.app.authorization.ui.initial_authorization.mvi.InitialAuthorizationStateMachine.Effect
-import org.timemates.app.authorization.ui.initial_authorization.mvi.InitialAuthorizationStateMachine.Event
-import org.timemates.app.foundation.mvi.EmptyState
-import org.timemates.app.foundation.mvi.StateMachine
+import org.timemates.app.authorization.ui.initial_authorization.mvi.InitialAuthorizationComponent.Action
+import org.timemates.app.authorization.ui.initial_authorization.mvi.InitialAuthorizationComponent.Intent
+import org.timemates.app.authorization.ui.initial_authorization.mvi.InitialAuthorizationComponent.State
+import org.timemates.app.feature.common.MVI
import org.timemates.app.localization.compose.LocalStrings
import org.timemates.app.style.system.Resources
import org.timemates.app.style.system.button.Button
import org.timemates.app.style.system.theme.AppTheme
-import kotlinx.coroutines.channels.consumeEach
+import pro.respawn.flowmvi.essenty.compose.subscribe
@Composable
fun InitialAuthorizationScreen(
- stateMachine: StateMachine,
+ mvi: MVI,
navigateToStartAuthorization: () -> Unit,
) {
val painter: Painter = Resources.image.initial_screen_image.painterResource()
- LaunchedEffect(Unit) {
- stateMachine.effects.consumeEach { effect ->
- when (effect) {
- is Effect.NavigateToStart ->
- navigateToStartAuthorization()
- }
+ @Suppress("UNUSED_VARIABLE")
+ val state by mvi.subscribe { effect ->
+ when (effect) {
+ is Action.NavigateToStart ->
+ navigateToStartAuthorization()
}
}
@@ -85,7 +84,7 @@ fun InitialAuthorizationScreen(
Button(
modifier = Modifier.fillMaxWidth(),
primary = true,
- onClick = { stateMachine.dispatchEvent(Event.OnStartClicked) },
+ onClick = { mvi.store.intent(Intent.OnStartClicked) },
) {
Text(LocalStrings.current.letsStart)
}
diff --git a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/initial_authorization/mvi/InitialAuthorizationComponent.kt b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/initial_authorization/mvi/InitialAuthorizationComponent.kt
new file mode 100644
index 0000000..3b4d6d3
--- /dev/null
+++ b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/initial_authorization/mvi/InitialAuthorizationComponent.kt
@@ -0,0 +1,43 @@
+package org.timemates.app.authorization.ui.initial_authorization.mvi
+
+import androidx.compose.runtime.Immutable
+import com.arkivanov.decompose.ComponentContext
+import kotlinx.serialization.Serializable
+import org.timemates.app.authorization.ui.initial_authorization.mvi.InitialAuthorizationComponent.Action
+import org.timemates.app.authorization.ui.initial_authorization.mvi.InitialAuthorizationComponent.Intent
+import org.timemates.app.authorization.ui.initial_authorization.mvi.InitialAuthorizationComponent.State
+import org.timemates.app.feature.common.MVI
+import pro.respawn.flowmvi.api.Container
+import pro.respawn.flowmvi.api.MVIAction
+import pro.respawn.flowmvi.api.MVIIntent
+import pro.respawn.flowmvi.api.MVIState
+import pro.respawn.flowmvi.api.Store
+import pro.respawn.flowmvi.dsl.emit
+import pro.respawn.flowmvi.essenty.dsl.retainedStore
+import pro.respawn.flowmvi.plugins.reduce
+
+class InitialAuthorizationComponent(
+ componentContext: ComponentContext,
+) : ComponentContext by componentContext, MVI {
+ override val store: Store = retainedStore(initial = State) {
+ reduce { intent: Intent ->
+ when (intent) {
+ is Intent.OnStartClicked -> {
+ emit(Action.NavigateToStart)
+ }
+ }
+ }
+ }
+
+ @Immutable
+ @Serializable
+ data object State : MVIState
+
+ sealed class Action : MVIAction {
+ data object NavigateToStart : Action()
+ }
+
+ sealed class Intent : MVIIntent {
+ data object OnStartClicked : Intent()
+ }
+}
\ No newline at end of file
diff --git a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/initial_authorization/mvi/InitialAuthorizationReducer.kt b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/initial_authorization/mvi/InitialAuthorizationReducer.kt
deleted file mode 100644
index 8abcd22..0000000
--- a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/initial_authorization/mvi/InitialAuthorizationReducer.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package org.timemates.app.authorization.ui.initial_authorization.mvi
-
-import org.timemates.app.authorization.ui.initial_authorization.mvi.InitialAuthorizationStateMachine.Effect
-import org.timemates.app.authorization.ui.initial_authorization.mvi.InitialAuthorizationStateMachine.Event
-import org.timemates.app.foundation.mvi.EmptyState
-import org.timemates.app.foundation.mvi.Reducer
-import org.timemates.app.foundation.mvi.ReducerScope
-
-class InitialAuthorizationReducer : Reducer {
- override fun ReducerScope.reduce(state: EmptyState, event: Event): EmptyState {
- return when (event) {
- is Event.OnStartClicked -> {
- sendEffect(Effect.NavigateToStart)
- state
- }
- }
- }
-}
\ No newline at end of file
diff --git a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/initial_authorization/mvi/InitialAuthorizationStateMachine.kt b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/initial_authorization/mvi/InitialAuthorizationStateMachine.kt
deleted file mode 100644
index 20d611e..0000000
--- a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/initial_authorization/mvi/InitialAuthorizationStateMachine.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package org.timemates.app.authorization.ui.initial_authorization.mvi
-
-import org.timemates.app.authorization.ui.initial_authorization.mvi.InitialAuthorizationStateMachine.Effect
-import org.timemates.app.authorization.ui.initial_authorization.mvi.InitialAuthorizationStateMachine.Event
-import org.timemates.app.foundation.mvi.EmptyState
-import org.timemates.app.foundation.mvi.StateMachine
-import org.timemates.app.foundation.mvi.UiEffect
-import org.timemates.app.foundation.mvi.UiEvent
-
-class InitialAuthorizationStateMachine(
- reducer: InitialAuthorizationReducer,
-) : StateMachine(
- initState = EmptyState,
- reducer = reducer,
- middlewares = emptyList(),
-) {
- sealed class Effect : UiEffect {
- object NavigateToStart : Effect()
- }
-
- sealed class Event : UiEvent {
- object OnStartClicked : Event()
- }
-}
diff --git a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/new_account_info/NewAccountInfoScreen.kt b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/new_account_info/NewAccountInfoScreen.kt
index 0c83f85..875a222 100644
--- a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/new_account_info/NewAccountInfoScreen.kt
+++ b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/new_account_info/NewAccountInfoScreen.kt
@@ -8,48 +8,44 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.ArrowBack
+import androidx.compose.material.icons.automirrored.rounded.ArrowBack
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.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import io.github.skeptick.libres.compose.painterResource
-import org.timemates.app.authorization.ui.new_account_info.mvi.NewAccountInfoStateMachine.Effect
-import org.timemates.app.authorization.ui.new_account_info.mvi.NewAccountInfoStateMachine.Event
-import org.timemates.app.foundation.mvi.EmptyState
-import org.timemates.app.foundation.mvi.StateMachine
+import org.timemates.app.authorization.ui.new_account_info.mvi.NewAccountInfoScreenComponent.*
+import org.timemates.app.feature.common.MVI
import org.timemates.app.localization.compose.LocalStrings
import org.timemates.app.style.system.Resources
import org.timemates.app.style.system.appbar.AppBar
import org.timemates.app.style.system.button.Button
import org.timemates.app.style.system.theme.AppTheme
-import kotlinx.coroutines.channels.consumeEach
+import pro.respawn.flowmvi.essenty.compose.subscribe
@Composable
fun NewAccountInfoScreen(
- stateMachine: StateMachine,
+ mvi: MVI,
navigateToConfigure: (String) -> Unit,
navigateToStart: () -> Unit,
) {
val painter: Painter = Resources.image.new_account_info_image.painterResource()
- LaunchedEffect(Unit) {
- stateMachine.effects.consumeEach { effect ->
- when (effect) {
- is Effect.NavigateToAccountConfiguring ->
- navigateToConfigure(effect.verificationHash.string)
+ @Suppress("UNUSED_VARIABLE")
+ val state by mvi.subscribe { action ->
+ when (action) {
+ is Action.NavigateToAccountConfiguring ->
+ navigateToConfigure(action.verificationHash.string)
- Effect.NavigateToStart ->
- navigateToStart()
- }
+ Action.NavigateToStart -> navigateToStart()
}
}
@@ -58,14 +54,17 @@ fun NewAccountInfoScreen(
AppBar(
navigationIcon = {
IconButton(
- onClick = { stateMachine.dispatchEvent(Event.OnBackClicked) },
+ onClick = { mvi.store.intent(Intent.OnBackClicked) },
) {
- Icon(Icons.Rounded.ArrowBack, contentDescription = null)
+ Icon(
+ imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
+ contentDescription = null,
+ )
}
},
title = LocalStrings.current.appName,
)
- }
+ },
) { rootPaddings ->
Column(
modifier = Modifier.fillMaxSize()
@@ -106,7 +105,7 @@ fun NewAccountInfoScreen(
Button(
modifier = Modifier.fillMaxWidth(),
primary = true,
- onClick = { stateMachine.dispatchEvent(Event.NextClicked) },
+ onClick = { mvi.store.intent(Intent.NextClicked) },
) {
Text(LocalStrings.current.nextStep)
}
diff --git a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/new_account_info/mvi/NewAccountInfoReducer.kt b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/new_account_info/mvi/NewAccountInfoReducer.kt
deleted file mode 100644
index c2e8f60..0000000
--- a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/new_account_info/mvi/NewAccountInfoReducer.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package org.timemates.app.authorization.ui.new_account_info.mvi
-
-import org.timemates.app.authorization.ui.new_account_info.mvi.NewAccountInfoStateMachine.Effect
-import org.timemates.app.authorization.ui.new_account_info.mvi.NewAccountInfoStateMachine.Event
-import org.timemates.app.foundation.mvi.EmptyState
-import org.timemates.app.foundation.mvi.Reducer
-import org.timemates.app.foundation.mvi.ReducerScope
-import io.timemates.sdk.authorization.email.types.value.VerificationHash
-
-class NewAccountInfoReducer(private val verificationHash: VerificationHash) : Reducer {
- override fun ReducerScope.reduce(state: EmptyState, event: Event): EmptyState {
- return when (event) {
- is Event.NextClicked -> {
- sendEffect(Effect.NavigateToAccountConfiguring(verificationHash))
- state
- }
-
- is Event.OnBackClicked -> {
- sendEffect(Effect.NavigateToStart)
- state
- }
- }
- }
-}
\ No newline at end of file
diff --git a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/new_account_info/mvi/NewAccountInfoScreenComponent.kt b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/new_account_info/mvi/NewAccountInfoScreenComponent.kt
new file mode 100644
index 0000000..58e07ec
--- /dev/null
+++ b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/new_account_info/mvi/NewAccountInfoScreenComponent.kt
@@ -0,0 +1,42 @@
+package org.timemates.app.authorization.ui.new_account_info.mvi
+
+import com.arkivanov.decompose.ComponentContext
+import org.timemates.app.authorization.ui.new_account_info.mvi.NewAccountInfoScreenComponent.Action
+import org.timemates.app.authorization.ui.new_account_info.mvi.NewAccountInfoScreenComponent.Intent
+import org.timemates.app.authorization.ui.new_account_info.mvi.NewAccountInfoScreenComponent.State
+import org.timemates.app.feature.common.MVI
+import org.timemates.sdk.authorization.email.types.value.VerificationHash
+import pro.respawn.flowmvi.api.Container
+import pro.respawn.flowmvi.api.MVIAction
+import pro.respawn.flowmvi.api.MVIIntent
+import pro.respawn.flowmvi.api.MVIState
+import pro.respawn.flowmvi.essenty.dsl.retainedStore
+import pro.respawn.flowmvi.plugins.reduce
+
+class NewAccountInfoScreenComponent(
+ componentContext: ComponentContext,
+ private val verificationHash: VerificationHash,
+) : ComponentContext by componentContext, MVI {
+ override val store = retainedStore(initial = State) {
+ reduce { intent ->
+ when (intent) {
+ Intent.NextClicked -> action(Action.NavigateToAccountConfiguring(verificationHash))
+ Intent.OnBackClicked -> action(Action.NavigateToStart)
+ }
+ }
+ }
+
+ data object State : MVIState
+
+ sealed class Action : MVIAction {
+ data class NavigateToAccountConfiguring(val verificationHash: VerificationHash) : Action()
+
+ data object NavigateToStart : Action()
+ }
+
+ sealed class Intent : MVIIntent {
+ data object NextClicked : Intent()
+
+ data object OnBackClicked : Intent()
+ }
+}
\ No newline at end of file
diff --git a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/new_account_info/mvi/NewAccountInfoStateMachine.kt b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/new_account_info/mvi/NewAccountInfoStateMachine.kt
deleted file mode 100644
index daf4bf4..0000000
--- a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/new_account_info/mvi/NewAccountInfoStateMachine.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-package org.timemates.app.authorization.ui.new_account_info.mvi
-
-import org.timemates.app.authorization.ui.new_account_info.mvi.NewAccountInfoStateMachine.Effect
-import org.timemates.app.authorization.ui.new_account_info.mvi.NewAccountInfoStateMachine.Event
-import org.timemates.app.foundation.mvi.EmptyState
-import org.timemates.app.foundation.mvi.StateMachine
-import org.timemates.app.foundation.mvi.UiEffect
-import org.timemates.app.foundation.mvi.UiEvent
-import io.timemates.sdk.authorization.email.types.value.VerificationHash
-
-class NewAccountInfoStateMachine(
- reducer: NewAccountInfoReducer,
-) : StateMachine(
- initState = EmptyState,
- reducer = reducer,
- middlewares = emptyList(),
-) {
- sealed class Effect : UiEffect {
- data class NavigateToAccountConfiguring(val verificationHash: VerificationHash) : Effect()
-
- data object NavigateToStart : Effect()
- }
-
- sealed class Event : UiEvent {
- data object NextClicked : Event()
-
- data object OnBackClicked : Event()
- }
-}
\ No newline at end of file
diff --git a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/start/StartAuthorizationScreen.kt b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/start/StartAuthorizationScreen.kt
index cade014..d7928f2 100644
--- a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/start/StartAuthorizationScreen.kt
+++ b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/start/StartAuthorizationScreen.kt
@@ -18,58 +18,54 @@ import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
-import org.timemates.app.authorization.ui.start.mvi.StartAuthorizationStateMachine.Effect
-import org.timemates.app.authorization.ui.start.mvi.StartAuthorizationStateMachine.Event
-import org.timemates.app.authorization.ui.start.mvi.StartAuthorizationStateMachine.State
+import org.timemates.app.authorization.ui.start.mvi.StartAuthorizationComponent.*
+import org.timemates.app.feature.common.MVI
import org.timemates.app.feature.common.failures.getDefaultDisplayMessage
-import org.timemates.app.foundation.mvi.StateMachine
+import org.timemates.app.feature.common.getFailuresIfPresent
+import org.timemates.app.feature.common.isInvalid
import org.timemates.app.localization.compose.LocalStrings
import org.timemates.app.style.system.appbar.AppBar
import org.timemates.app.style.system.button.ButtonWithProgress
-import io.timemates.sdk.authorization.email.types.value.VerificationHash
-import kotlinx.coroutines.channels.consumeEach
+import org.timemates.sdk.authorization.email.types.value.VerificationHash
+import pro.respawn.flowmvi.essenty.compose.subscribe
@Composable
fun StartAuthorizationScreen(
- stateMachine: StateMachine,
+ mvi: MVI,
onNavigateToConfirmation: (VerificationHash) -> Unit,
) {
- val state by stateMachine.state.collectAsState()
val snackbarData = remember { SnackbarHostState() }
-
val strings = LocalStrings.current
- LaunchedEffect(true) {
- stateMachine.effects.consumeEach { effect ->
- when (effect) {
- is Effect.Failure -> {
- effect.throwable.printStackTrace()
+ val state by mvi.subscribe { action ->
+ when (action) {
+ is Action.Failure -> {
+ action.throwable.printStackTrace()
- snackbarData.showSnackbar(
- message = effect.throwable.getDefaultDisplayMessage(strings),
- actionLabel = strings.dismiss,
- duration = SnackbarDuration.Long,
- )
- }
+ snackbarData.showSnackbar(
+ message = action.throwable.getDefaultDisplayMessage(strings),
+ actionLabel = strings.dismiss,
+ duration = SnackbarDuration.Long,
+ )
+ }
- is Effect.NavigateToConfirmation -> onNavigateToConfirmation(effect.verificationHash)
- Effect.TooManyAttempts -> {
- snackbarData.showSnackbar(
- message = strings.tooManyAttempts,
- actionLabel = strings.dismiss,
- duration = SnackbarDuration.Long,
- )
- }
+ is Action.NavigateToConfirmation -> onNavigateToConfirmation(action.verificationHash)
+
+ Action.TooManyAttempts -> {
+ snackbarData.showSnackbar(
+ message = strings.tooManyAttempts,
+ actionLabel = strings.dismiss,
+ duration = SnackbarDuration.Long,
+ )
}
}
}
+
Scaffold(
topBar = {
AppBar(
@@ -83,19 +79,15 @@ fun StartAuthorizationScreen(
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically),
horizontalAlignment = Alignment.CenterHorizontally,
) {
- val supportText = when {
- state.isEmailInvalid -> LocalStrings.current.emailIsInvalid
- state.isEmailLengthSizeInvalid -> LocalStrings.current.emailSizeIsInvalid
- else -> null
- }
+ val supportText = state.email.getFailuresIfPresent(strings)
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
leadingIcon = { Icon(Icons.Outlined.Email, contentDescription = null) },
- value = state.email,
- onValueChange = { stateMachine.dispatchEvent(Event.EmailChange(it)) },
+ value = state.email.value,
+ onValueChange = { mvi.store.intent(Intent.EmailChange(it)) },
label = { Text(LocalStrings.current.email) },
- isError = state.isEmailInvalid || state.isEmailLengthSizeInvalid,
+ isError = state.email.isInvalid(),
supportingText = { if (supportText != null) Text(supportText) },
enabled = !state.isLoading,
singleLine = true,
@@ -117,7 +109,7 @@ fun StartAuthorizationScreen(
enabled = !state.isLoading,
primary = true,
modifier = Modifier.fillMaxWidth(),
- onClick = { stateMachine.dispatchEvent(Event.OnStartClick) },
+ onClick = { mvi.store.intent(Intent.OnStartClick) },
isLoading = state.isLoading
) {
Text(text = LocalStrings.current.start)
diff --git a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/start/mvi/StartAuthorizationComponent.kt b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/start/mvi/StartAuthorizationComponent.kt
new file mode 100644
index 0000000..4b3176e
--- /dev/null
+++ b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/start/mvi/StartAuthorizationComponent.kt
@@ -0,0 +1,102 @@
+package org.timemates.app.authorization.ui.start.mvi
+
+import androidx.compose.runtime.Immutable
+import com.arkivanov.decompose.ComponentContext
+import kotlinx.coroutines.launch
+import org.timemates.app.authorization.ui.start.mvi.StartAuthorizationComponent.Action
+import org.timemates.app.authorization.ui.start.mvi.StartAuthorizationComponent.Intent
+import org.timemates.app.authorization.ui.start.mvi.StartAuthorizationComponent.State
+import org.timemates.app.authorization.usecases.AuthorizeByEmailUseCase
+import org.timemates.app.feature.common.Input
+import org.timemates.app.feature.common.MVI
+import org.timemates.app.feature.common.input
+import org.timemates.app.feature.common.isValid
+import org.timemates.sdk.authorization.email.types.value.VerificationHash
+import org.timemates.sdk.common.constructor.createOrThrow
+import org.timemates.sdk.users.profile.types.value.EmailAddress
+import pro.respawn.flowmvi.api.Container
+import pro.respawn.flowmvi.api.MVIAction
+import pro.respawn.flowmvi.api.MVIIntent
+import pro.respawn.flowmvi.api.MVIState
+import pro.respawn.flowmvi.api.PipelineContext
+import pro.respawn.flowmvi.api.Store
+import pro.respawn.flowmvi.dsl.emit
+import pro.respawn.flowmvi.essenty.dsl.retainedStore
+import pro.respawn.flowmvi.plugins.recover
+import pro.respawn.flowmvi.plugins.reduce
+
+class StartAuthorizationComponent(
+ componentContext: ComponentContext,
+ private val authorizeByEmail: AuthorizeByEmailUseCase,
+) : ComponentContext by componentContext, MVI {
+ override val store: Store = retainedStore(initial = State()) {
+ recover { exception ->
+ emit(Action.Failure(exception))
+ null
+ }
+
+ reduce { intent ->
+ updateState {
+ when (intent) {
+ is Intent.EmailChange -> copy(
+ email = input(intent.email),
+ )
+
+ Intent.OnStartClick -> copy(email = email.validated(EmailAddress.factory)).run {
+ when {
+ email.isValid() -> {
+ authWithEmailAsync(email.value)
+ copy(isLoading = true)
+ }
+
+ else -> this
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun PipelineContext.authWithEmailAsync(email: String) {
+ launch {
+ when (val result = authorizeByEmail.execute(EmailAddress.factory.createOrThrow(email))) {
+ is AuthorizeByEmailUseCase.Result.Success -> {
+ emit(Action.NavigateToConfirmation(result.verificationHash))
+ updateState { copy(isLoading = false) }
+ }
+
+ AuthorizeByEmailUseCase.Result.TooManyRequests -> {
+ emit(Action.TooManyAttempts)
+ updateState { copy(isLoading = false) }
+ }
+
+ is AuthorizeByEmailUseCase.Result.Failure -> {
+ emit(Action.Failure(result.throwable))
+ updateState { copy(isLoading = false) }
+ }
+ }
+ }
+ }
+
+ @Immutable
+ data class State(
+ val email: Input = input(""),
+ val isLoading: Boolean = false,
+ ) : MVIState
+
+ sealed class Intent : MVIIntent {
+ data class EmailChange(val email: String) : Intent()
+
+ data object OnStartClick : Intent()
+ }
+
+ sealed class Action : MVIAction {
+ data object TooManyAttempts : Action()
+
+ data class Failure(val throwable: Throwable) : Action()
+
+ data class NavigateToConfirmation(
+ val verificationHash: VerificationHash,
+ ) : Action()
+ }
+}
\ No newline at end of file
diff --git a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/start/mvi/StartAuthorizationMiddleware.kt b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/start/mvi/StartAuthorizationMiddleware.kt
deleted file mode 100644
index 9fd64ef..0000000
--- a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/start/mvi/StartAuthorizationMiddleware.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-package org.timemates.app.authorization.ui.start.mvi
-
-import org.timemates.app.authorization.ui.start.mvi.StartAuthorizationStateMachine.Effect
-import org.timemates.app.authorization.ui.start.mvi.StartAuthorizationStateMachine.State
-import org.timemates.app.foundation.mvi.Middleware
-import org.timemates.app.foundation.mvi.StateStore
-
-/**
- * A middleware responsible for handling effects in the Start Authorization screen.
- */
-class StartAuthorizationMiddleware : Middleware {
-
- /**
- * Handles the specified effect and updates the state if necessary.
- *
- * This function is called when an effect is dispatched within the Start Authorization feature.
- * It receives the effect, the current state store, and a function to update the state.
- * The purpose of this function is to react to specific effects and modify the state accordingly.
- * In this case, when an [Effect.Failure] or [Effect.TooManyAttempts] is received,
- * it sets the loading state in the UI to false by updating the state.
- *
- * @param effect The effect to be handled.
- * @param store The state store containing the current state.
- */
- override fun onEffect(effect: Effect, state: State): State {
- return when (effect) {
- is Effect.Failure, Effect.TooManyAttempts, is Effect.NavigateToConfirmation ->
- state.copy(isLoading = false)
- }
- }
-}
diff --git a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/start/mvi/StartAuthorizationReducer.kt b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/start/mvi/StartAuthorizationReducer.kt
deleted file mode 100644
index fca0ef4..0000000
--- a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/start/mvi/StartAuthorizationReducer.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-package org.timemates.app.authorization.ui.start.mvi
-
-import org.timemates.app.authorization.ui.start.mvi.StartAuthorizationStateMachine.Effect
-import org.timemates.app.authorization.ui.start.mvi.StartAuthorizationStateMachine.Event
-import org.timemates.app.authorization.ui.start.mvi.StartAuthorizationStateMachine.State
-import org.timemates.app.authorization.usecases.AuthorizeByEmailUseCase
-import org.timemates.app.authorization.validation.EmailAddressValidator
-import org.timemates.app.foundation.mvi.Reducer
-import org.timemates.app.foundation.mvi.ReducerScope
-import io.timemates.sdk.common.constructor.createOrThrow
-import io.timemates.sdk.users.profile.types.value.EmailAddress
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
-
-class StartAuthorizationReducer(
- private val validateEmail: EmailAddressValidator,
- private val authorizeByEmail: AuthorizeByEmailUseCase,
-) : Reducer {
- override fun ReducerScope.reduce(state: State, event: Event): State {
- return when (event) {
- is Event.EmailChange -> state.copy(
- email = event.email,
- isEmailInvalid = false,
- isEmailLengthSizeInvalid = false
- )
-
- Event.OnStartClick -> when (validateEmail.validate(state.email)) {
- EmailAddressValidator.Result.PatternDoesNotMatch ->
- state.copy(isEmailInvalid = true)
-
- EmailAddressValidator.Result.SizeViolation ->
- state.copy(isEmailLengthSizeInvalid = true)
-
- EmailAddressValidator.Result.Success -> {
- authorizeWithEmail(state.email, sendEffect, machineScope)
- state.copy(isLoading = true, isEmailInvalid = false)
- }
- }
- }
- }
-
- private fun authorizeWithEmail(
- email: String,
- sendEffect: (Effect) -> Unit,
- scope: CoroutineScope,
- ) {
- scope.launch {
- when (val result = authorizeByEmail.execute(EmailAddress.createOrThrow(email))) {
- is AuthorizeByEmailUseCase.Result.Success ->
- sendEffect(Effect.NavigateToConfirmation(result.verificationHash))
-
- AuthorizeByEmailUseCase.Result.TooManyRequests ->
- sendEffect(Effect.TooManyAttempts)
-
- is AuthorizeByEmailUseCase.Result.Failure ->
- sendEffect(Effect.Failure(result.throwable))
- }
- }
- }
-}
\ No newline at end of file
diff --git a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/start/mvi/StartAuthorizationStateMachine.kt b/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/start/mvi/StartAuthorizationStateMachine.kt
deleted file mode 100644
index 50c256c..0000000
--- a/feature/authorization/presentation/src/commonMain/kotlin/org/timemates/app/authorization/ui/start/mvi/StartAuthorizationStateMachine.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-package org.timemates.app.authorization.ui.start.mvi
-
-import androidx.compose.runtime.Immutable
-import org.timemates.app.authorization.ui.start.mvi.StartAuthorizationStateMachine.Effect
-import org.timemates.app.authorization.ui.start.mvi.StartAuthorizationStateMachine.Event
-import org.timemates.app.authorization.ui.start.mvi.StartAuthorizationStateMachine.State
-import org.timemates.app.foundation.mvi.StateMachine
-import org.timemates.app.foundation.mvi.UiEffect
-import org.timemates.app.foundation.mvi.UiEvent
-import org.timemates.app.foundation.mvi.UiState
-import io.timemates.sdk.authorization.email.types.value.VerificationHash
-
-class StartAuthorizationStateMachine(
- reducer: StartAuthorizationReducer,
- middleware: StartAuthorizationMiddleware,
-) : StateMachine(
- initState = State(),
- reducer = reducer,
- middlewares = listOf(middleware)
-) {
- @Immutable
- data class State(
- val email: String = "",
- val isEmailInvalid: Boolean = false,
- val isEmailLengthSizeInvalid: Boolean = false,
- val isLoading: Boolean = false,
- ) : UiState
-
- sealed class Event : UiEvent {
- data class EmailChange(val email: String) : Event()
-
- data object OnStartClick : Event()
- }
-
- sealed class Effect : UiEffect {
- data object TooManyAttempts : Effect()
-
- data class Failure(val throwable: Throwable) : Effect()
-
- data class NavigateToConfirmation(
- val verificationHash: VerificationHash,
- ) : Effect()
- }
-}
\ No newline at end of file
diff --git a/feature/authorization/presentation/src/jvmTest/kotlin/org/timemates/app/authorization/ui/new_account_info/mvi/NewAccountInfoReducerTest.kt b/feature/authorization/presentation/src/jvmTest/kotlin/org/timemates/app/authorization/ui/new_account_info/mvi/NewAccountInfoReducerTest.kt
deleted file mode 100644
index c67957a..0000000
--- a/feature/authorization/presentation/src/jvmTest/kotlin/org/timemates/app/authorization/ui/new_account_info/mvi/NewAccountInfoReducerTest.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-package org.timemates.app.authorization.ui.new_account_info.mvi
-
-import io.mockk.mockk
-import io.mockk.verify
-import org.timemates.app.authorization.ui.afterstart.mvi.AfterStartReducer
-import org.timemates.app.authorization.ui.afterstart.mvi.AfterStartStateMachine
-import org.timemates.app.foundation.mvi.EmptyState
-import org.timemates.app.foundation.mvi.reduce
-import org.timemates.app.foundation.random.nextString
-import io.timemates.sdk.authorization.email.types.value.VerificationHash
-import io.timemates.sdk.common.constructor.createOrThrow
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.test.TestScope
-import kotlin.random.Random
-import kotlin.test.Test
-import kotlin.test.assertEquals
-
-class NewAccountInfoReducerTest {
- private val verificationHash: VerificationHash = VerificationHash.createOrThrow(Random.nextString(VerificationHash.SIZE))
- private val sendEffect: (AfterStartStateMachine.Effect) -> Unit = mockk(relaxed = true)
- private val reducer: AfterStartReducer = AfterStartReducer(verificationHash)
- private val coroutineScope = TestScope()
-
- @Test
- fun `reducing event NextClicked event should not update the state`() {
- // GIVEN
- val state: EmptyState = EmptyState
- val event: AfterStartStateMachine.Event.NextClicked = AfterStartStateMachine.Event.NextClicked
-
- // WHEN
- val resultState = reducer.reduce(state, event, coroutineScope, sendEffect)
-
- // THEN
- verify { sendEffect(AfterStartStateMachine.Effect.NavigateToConfirmation(verificationHash)) }
- assertEquals(state, resultState)
- }
-
- @Test
- fun `reducing event OnBackClicked event should not update the state`() {
- // GIVEN
- val state: EmptyState = EmptyState
-
- val event: AfterStartStateMachine.Event.OnChangeEmailClicked = AfterStartStateMachine.Event.OnChangeEmailClicked
-
- // WHEN
- val resultState = reducer.reduce(state, event, coroutineScope, sendEffect)
-
- // THEN
- verify { sendEffect(AfterStartStateMachine.Effect.OnChangeEmailClicked) }
- assertEquals(state, resultState)
- }
-}
\ No newline at end of file
diff --git a/feature/authorization/presentation/src/jvmTest/kotlin/org/timemates/app/authorization/ui/afterstart/AfterStartReducerTest.kt b/feature/authorization/presentation/src/jvmTestOld/kotlin/org/timemates/app/authorization/ui/afterstart/AfterStartReducerTest.kt
similarity index 52%
rename from feature/authorization/presentation/src/jvmTest/kotlin/org/timemates/app/authorization/ui/afterstart/AfterStartReducerTest.kt
rename to feature/authorization/presentation/src/jvmTestOld/kotlin/org/timemates/app/authorization/ui/afterstart/AfterStartReducerTest.kt
index e9f2454..9ff996a 100644
--- a/feature/authorization/presentation/src/jvmTest/kotlin/org/timemates/app/authorization/ui/afterstart/AfterStartReducerTest.kt
+++ b/feature/authorization/presentation/src/jvmTestOld/kotlin/org/timemates/app/authorization/ui/afterstart/AfterStartReducerTest.kt
@@ -2,51 +2,49 @@ package org.timemates.app.authorization.ui.afterstart
import io.mockk.mockk
import io.mockk.verify
-import org.timemates.app.authorization.ui.afterstart.mvi.AfterStartReducer
-import org.timemates.app.authorization.ui.afterstart.mvi.AfterStartStateMachine
-import org.timemates.app.foundation.mvi.EmptyState
+import org.timemates.sdk.authorization.email.types.value.VerificationHash
+import org.timemates.sdk.common.constructor.createOrThrow
+import kotlinx.coroutines.test.TestScope
+import org.timemates.app.authorization.ui.afterstart.mvi.AfterStartScreenComponent
+import org.timemates.app.authorization.ui.afterstart.mvi.AfterStartScreenComponent.State
import org.timemates.app.foundation.mvi.reduce
import org.timemates.app.foundation.random.nextString
-import io.timemates.sdk.authorization.email.types.value.VerificationHash
-import io.timemates.sdk.common.constructor.createOrThrow
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.test.TestScope
import kotlin.random.Random
import kotlin.test.Test
import kotlin.test.assertEquals
class AfterStartReducerTest {
- private val verificationHash: VerificationHash = VerificationHash.createOrThrow(Random.nextString(VerificationHash.SIZE))
- private val sendEffect: (AfterStartStateMachine.Effect) -> Unit = mockk(relaxed = true)
+ private val verificationHash: VerificationHash = VerificationHash.factory.createOrThrow(Random.nextString(VerificationHash.SIZE))
+ private val sendEffect: (AfterStartScreenComponent.Action) -> Unit = mockk(relaxed = true)
private val reducer: AfterStartReducer = AfterStartReducer(verificationHash)
private val coroutineScope = TestScope()
@Test
fun `reducing event NextClicked event should not update the state`() {
// GIVEN
- val state = EmptyState
- val event = AfterStartStateMachine.Event.NextClicked
+ val state = State
+ val intent = AfterStartScreenComponent.Intent.NextClicked
// WHEN
- val resultState = reducer.reduce(state, event, coroutineScope, sendEffect)
+ val resultState = reducer.reduce(state, intent, coroutineScope, sendEffect)
// THEN
- verify { sendEffect(AfterStartStateMachine.Effect.NavigateToConfirmation(verificationHash)) }
+ verify { sendEffect(AfterStartScreenComponent.Action.NavigateToConfirmation(verificationHash)) }
assertEquals(state, resultState)
}
@Test
fun `reducing event OnChangeEmailClicked event should not update the state`() {
// GIVEN
- val state = EmptyState
- val event = AfterStartStateMachine.Event.OnChangeEmailClicked
+ val state = State
+ val intent = AfterStartScreenComponent.Intent.OnChangeEmailClicked
// WHEN
- val resultState = reducer.reduce(state, event, coroutineScope, sendEffect)
+ val resultState = reducer.reduce(state, intent, coroutineScope, sendEffect)
// THEN
- verify { sendEffect(AfterStartStateMachine.Effect.OnChangeEmailClicked) }
+ verify { sendEffect(AfterStartScreenComponent.Action.OnChangeEmailClicked) }
assertEquals(state, resultState)
}
}
diff --git a/feature/authorization/presentation/src/jvmTest/kotlin/org/timemates/app/authorization/ui/configure_account/ConfigureAccountMiddlewareTest.kt b/feature/authorization/presentation/src/jvmTestOld/kotlin/org/timemates/app/authorization/ui/configure_account/ConfigureAccountMiddlewareTest.kt
similarity index 63%
rename from feature/authorization/presentation/src/jvmTest/kotlin/org/timemates/app/authorization/ui/configure_account/ConfigureAccountMiddlewareTest.kt
rename to feature/authorization/presentation/src/jvmTestOld/kotlin/org/timemates/app/authorization/ui/configure_account/ConfigureAccountMiddlewareTest.kt
index 580b96c..c506e54 100644
--- a/feature/authorization/presentation/src/jvmTest/kotlin/org/timemates/app/authorization/ui/configure_account/ConfigureAccountMiddlewareTest.kt
+++ b/feature/authorization/presentation/src/jvmTestOld/kotlin/org/timemates/app/authorization/ui/configure_account/ConfigureAccountMiddlewareTest.kt
@@ -1,27 +1,23 @@
package org.timemates.app.authorization.ui.configure_account
-import io.mockk.every
import io.mockk.mockk
-import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountMiddleware
-import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountStateMachine
-import org.timemates.app.foundation.mvi.StateStore
-import io.timemates.sdk.authorization.sessions.types.Authorization
-import kotlinx.coroutines.flow.MutableStateFlow
+import org.timemates.sdk.authorization.sessions.types.Authorization
import org.junit.jupiter.api.Test
+import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountMiddleware
+import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountScreenComponent
+import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountScreenComponent.State
class ConfigureAccountMiddlewareTest {
- private val stateStore: StateStore = mockk()
private val middleware: ConfigureAccountMiddleware = ConfigureAccountMiddleware()
private val authorization: Authorization = mockk()
@Test
fun `effects produced by network operations should remove loading status`() {
// GIVEN
- val effects = listOf(ConfigureAccountStateMachine.Effect.Failure(Exception()))
- every { stateStore.state } returns MutableStateFlow(ConfigureAccountStateMachine.State(isLoading = true))
+ val effects = listOf(ConfigureAccountScreenComponent.Effect.Failure(Exception()))
// WHEN
- effects.map { effect -> effect to middleware.onEffect(effect, stateStore) }
+ effects.map { effect -> effect to middleware.onEffect(effect, State(isLoading = true)) }
// THEN
.forEach { (effect, state) ->
assert(!state.isLoading) {
@@ -34,13 +30,12 @@ class ConfigureAccountMiddlewareTest {
fun `effects not produced by network operations should not remove loading status`() {
// GIVEN
val effects = listOf(
- ConfigureAccountStateMachine.Effect.NavigateToStart,
- ConfigureAccountStateMachine.Effect.NavigateToHomePage(authorization)
+ ConfigureAccountScreenComponent.Effect.NavigateToStart,
+ ConfigureAccountScreenComponent.Effect.NavigateToHomePage(authorization)
)
- every { stateStore.state } returns MutableStateFlow(ConfigureAccountStateMachine.State(isLoading = true))
// WHEN
- effects.map { effect -> effect to middleware.onEffect(effect, stateStore) }
+ effects.map { effect -> effect to middleware.onEffect(effect, State(isLoading = true)) }
// THEN
.forEach { (effect, state) ->
assert(state.isLoading) {
diff --git a/feature/authorization/presentation/src/jvmTest/kotlin/org/timemates/app/authorization/ui/configure_account/ConfigureAccountReducerTest.kt b/feature/authorization/presentation/src/jvmTestOld/kotlin/org/timemates/app/authorization/ui/configure_account/ConfigureAccountReducerTest.kt
similarity index 92%
rename from feature/authorization/presentation/src/jvmTest/kotlin/org/timemates/app/authorization/ui/configure_account/ConfigureAccountReducerTest.kt
rename to feature/authorization/presentation/src/jvmTestOld/kotlin/org/timemates/app/authorization/ui/configure_account/ConfigureAccountReducerTest.kt
index 12af23e..085f492 100644
--- a/feature/authorization/presentation/src/jvmTest/kotlin/org/timemates/app/authorization/ui/configure_account/ConfigureAccountReducerTest.kt
+++ b/feature/authorization/presentation/src/jvmTestOld/kotlin/org/timemates/app/authorization/ui/configure_account/ConfigureAccountReducerTest.kt
@@ -2,19 +2,17 @@ package org.timemates.app.authorization.ui.configure_account
import io.mockk.every
import io.mockk.mockk
-import org.timemates.app.authorization.ui.afterstart.mvi.AfterStartStateMachine
+import org.timemates.sdk.authorization.email.types.value.VerificationHash
+import kotlinx.coroutines.test.TestScope
+import org.junit.jupiter.api.Test
import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountReducer
-import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountStateMachine
-import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountStateMachine.Event
-import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountStateMachine.State
+import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountScreenComponent
+import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountScreenComponent.Event
+import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountScreenComponent.State
import org.timemates.app.authorization.usecases.CreateNewAccountUseCase
import org.timemates.app.authorization.validation.UserDescriptionValidator
import org.timemates.app.authorization.validation.UserNameValidator
import org.timemates.app.foundation.mvi.reduce
-import io.timemates.sdk.authorization.email.types.value.VerificationHash
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.test.TestScope
-import org.junit.jupiter.api.Test
import kotlin.test.assertEquals
class ConfigureAccountReducerTest {
@@ -28,7 +26,7 @@ class ConfigureAccountReducerTest {
private val invalidName = "ko"
private val invalidDescription = ""
private val coroutineScope = TestScope()
- private val sendEffect: (ConfigureAccountStateMachine.Effect) -> Unit = mockk(relaxed = true)
+ private val sendEffect: (ConfigureAccountScreenComponent.Effect) -> Unit = mockk(relaxed = true)
private val reducer = ConfigureAccountReducer(
verificationHash,
diff --git a/feature/authorization/presentation/src/jvmTest/kotlin/org/timemates/app/authorization/ui/confirmation/ConfirmAuthorizationMiddlewareTest.kt b/feature/authorization/presentation/src/jvmTestOld/kotlin/org/timemates/app/authorization/ui/confirmation/ConfirmAuthorizationMiddlewareTest.kt
similarity index 59%
rename from feature/authorization/presentation/src/jvmTest/kotlin/org/timemates/app/authorization/ui/confirmation/ConfirmAuthorizationMiddlewareTest.kt
rename to feature/authorization/presentation/src/jvmTestOld/kotlin/org/timemates/app/authorization/ui/confirmation/ConfirmAuthorizationMiddlewareTest.kt
index b860f75..f60174b 100644
--- a/feature/authorization/presentation/src/jvmTest/kotlin/org/timemates/app/authorization/ui/confirmation/ConfirmAuthorizationMiddlewareTest.kt
+++ b/feature/authorization/presentation/src/jvmTestOld/kotlin/org/timemates/app/authorization/ui/confirmation/ConfirmAuthorizationMiddlewareTest.kt
@@ -1,72 +1,57 @@
package org.timemates.app.authorization.ui.confirmation
-import io.mockk.every
import io.mockk.mockk
-import io.mockk.verify
-import org.timemates.app.authorization.ui.confirmation.mvi.ConfirmAuthorizationMiddleware
-import org.timemates.app.authorization.ui.confirmation.mvi.ConfirmAuthorizationStateMachine.Effect
-import org.timemates.app.authorization.ui.confirmation.mvi.ConfirmAuthorizationStateMachine.State
-import org.timemates.app.foundation.mvi.StateStore
-import kotlinx.coroutines.flow.MutableStateFlow
+import org.timemates.app.authorization.ui.confirmation.mvi.ConfirmAuthorizationScreenComponent.Action
+import org.timemates.app.authorization.ui.confirmation.mvi.ConfirmAuthorizationScreenComponent.State
import kotlin.test.Test
import kotlin.test.assertEquals
class ConfirmAuthorizationMiddlewareTest {
-
- private val store: StateStore = mockk(relaxed = true)
private val middleware = ConfirmAuthorizationMiddleware()
@Test
fun `onEffect with Effect Failure should update state with isLoading false`() {
// GIVEN
val initialState = State(isLoading = true)
- val effect = Effect.Failure(RuntimeException("Authorization failed"))
-
- every { store.state } returns MutableStateFlow(initialState)
+ val action = Action.Failure(RuntimeException("Authorization failed"))
// WHEN
- val newState = middleware.onEffect(effect, store)
+ val newState = middleware.onEffect(action, initialState)
// THEN
assertEquals(
expected = initialState.copy(isLoading = false),
actual = newState
)
- verify { store.state }
}
@Test
fun `onEffect with Effect TooManyAttempts should update state with isLoading false`() {
// GIVEN
val initialState = State(isLoading = true)
- val effect = Effect.TooManyAttempts
-
- every { store.state } returns MutableStateFlow(initialState)
+ val action = Action.TooManyAttempts
// WHEN
- val newState = middleware.onEffect(effect, store)
+ val newState = middleware.onEffect(action, initialState)
// THEN
assertEquals(
expected = initialState.copy(isLoading = false),
actual = newState
)
- verify { store.state }
}
@Test
fun `navigation effects should change isLoading status`() {
// GIVEN
val initialState = State(isLoading = true)
- val effects = listOf(
- mockk(),
- mockk(),
+ val actions = listOf(
+ mockk(),
+ mockk(),
)
- every { store.state } returns MutableStateFlow(initialState)
-
// WHEN
- effects.map { effect -> effect to middleware.onEffect(effect, store) }
+ actions.map { effect -> effect to middleware.onEffect(effect, initialState) }
.forEach { (effect, state) ->
// THEN
assertEquals(
diff --git a/feature/authorization/presentation/src/jvmTest/kotlin/org/timemates/app/authorization/ui/confirmation/ConfirmAuthorizationReducerTest.kt b/feature/authorization/presentation/src/jvmTestOld/kotlin/org/timemates/app/authorization/ui/confirmation/ConfirmAuthorizationReducerTest.kt
similarity index 75%
rename from feature/authorization/presentation/src/jvmTest/kotlin/org/timemates/app/authorization/ui/confirmation/ConfirmAuthorizationReducerTest.kt
rename to feature/authorization/presentation/src/jvmTestOld/kotlin/org/timemates/app/authorization/ui/confirmation/ConfirmAuthorizationReducerTest.kt
index fdb7dc2..260885d 100644
--- a/feature/authorization/presentation/src/jvmTest/kotlin/org/timemates/app/authorization/ui/confirmation/ConfirmAuthorizationReducerTest.kt
+++ b/feature/authorization/presentation/src/jvmTestOld/kotlin/org/timemates/app/authorization/ui/confirmation/ConfirmAuthorizationReducerTest.kt
@@ -2,30 +2,26 @@ package org.timemates.app.authorization.ui.confirmation
import io.mockk.every
import io.mockk.mockk
-import org.timemates.app.authorization.repositories.AuthorizationsRepository
-import org.timemates.app.authorization.ui.confirmation.mvi.ConfirmAuthorizationStateMachine.Event
-import org.timemates.app.authorization.ui.confirmation.mvi.ConfirmAuthorizationStateMachine.State
-import org.timemates.app.authorization.ui.confirmation.mvi.ConfirmAuthorizationsReducer
+import org.timemates.sdk.authorization.email.types.value.VerificationHash
+import org.timemates.sdk.common.constructor.createOrThrow
+import kotlinx.coroutines.test.TestScope
+import org.junit.jupiter.api.Test
+import org.timemates.app.authorization.ui.confirmation.mvi.ConfirmAuthorizationScreenComponent.Intent
+import org.timemates.app.authorization.ui.confirmation.mvi.ConfirmAuthorizationScreenComponent.State
import org.timemates.app.authorization.usecases.ConfirmEmailAuthorizationUseCase
import org.timemates.app.authorization.validation.ConfirmationCodeValidator
import org.timemates.app.foundation.mvi.reduce
import org.timemates.app.foundation.random.nextString
-import io.timemates.sdk.authorization.email.types.value.VerificationHash
-import io.timemates.sdk.common.constructor.createOrThrow
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.test.TestScope
-import org.junit.jupiter.api.Test
import kotlin.random.Random
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class ConfirmAuthorizationReducerTest {
- private val authorizationsRepository: AuthorizationsRepository = mockk()
private val confirmationCodeValidator: ConfirmationCodeValidator = mockk()
private val confirmEmailAuthorizationUseCase: ConfirmEmailAuthorizationUseCase = mockk()
private val coroutineScope = TestScope()
- private val verificationHash = VerificationHash.createOrThrow(Random.nextString(VerificationHash.SIZE))
+ private val verificationHash = VerificationHash.factory.createOrThrow(Random.nextString(VerificationHash.SIZE))
private val reducer = ConfirmAuthorizationsReducer(
verificationHash,
confirmEmailAuthorizationUseCase,
@@ -40,7 +36,7 @@ class ConfirmAuthorizationReducerTest {
ConfirmationCodeValidator.Result.Success
// WHEN
- val nextState = reducer.reduce(state, Event.OnConfirmClicked, coroutineScope) {}
+ val nextState = reducer.reduce(state, Intent.OnConfirmClicked, coroutineScope) {}
// THEN
assertTrue(nextState.isLoading)
@@ -57,7 +53,7 @@ class ConfirmAuthorizationReducerTest {
ConfirmationCodeValidator.Result.SizeIsInvalid
// WHEN
- val nextState = reducer.reduce(state, Event.OnConfirmClicked, coroutineScope) {}
+ val nextState = reducer.reduce(state, Intent.OnConfirmClicked, coroutineScope) {}
// THEN
assertFalse(nextState.isLoading)
@@ -70,10 +66,10 @@ class ConfirmAuthorizationReducerTest {
fun `reducing Event CodeChange should update the code in state`() {
// GIVEN
val state = State(code = "123456", isLoading = false)
- val event = Event.CodeChange("654321")
+ val intent = Intent.CodeChange("654321")
// WHEN
- val nextState = reducer.reduce(state, event, coroutineScope) {}
+ val nextState = reducer.reduce(state, intent, coroutineScope) {}
// THEN
assertEquals("654321", nextState.code)
@@ -90,7 +86,7 @@ class ConfirmAuthorizationReducerTest {
ConfirmationCodeValidator.Result.PatternFailure
// WHEN
- val nextState = reducer.reduce(state, Event.OnConfirmClicked, coroutineScope) {}
+ val nextState = reducer.reduce(state, Intent.OnConfirmClicked, coroutineScope) {}
// THEN
assertFalse(nextState.isLoading)
diff --git a/feature/authorization/presentation/src/jvmTest/kotlin/org/timemates/app/authorization/ui/initial_authorization/InitialAuthorizationReducerTest.kt b/feature/authorization/presentation/src/jvmTestOld/kotlin/org/timemates/app/authorization/ui/initial_authorization/InitialAuthorizationReducerTest.kt
similarity index 66%
rename from feature/authorization/presentation/src/jvmTest/kotlin/org/timemates/app/authorization/ui/initial_authorization/InitialAuthorizationReducerTest.kt
rename to feature/authorization/presentation/src/jvmTestOld/kotlin/org/timemates/app/authorization/ui/initial_authorization/InitialAuthorizationReducerTest.kt
index a1ea48b..a8f81e8 100644
--- a/feature/authorization/presentation/src/jvmTest/kotlin/org/timemates/app/authorization/ui/initial_authorization/InitialAuthorizationReducerTest.kt
+++ b/feature/authorization/presentation/src/jvmTestOld/kotlin/org/timemates/app/authorization/ui/initial_authorization/InitialAuthorizationReducerTest.kt
@@ -2,14 +2,9 @@ package org.timemates.app.authorization.ui.initial_authorization
import io.mockk.mockk
import io.mockk.verify
-import org.timemates.app.authorization.ui.initial_authorization.mvi.InitialAuthorizationReducer
-import org.timemates.app.authorization.ui.initial_authorization.mvi.InitialAuthorizationStateMachine.Effect
-import org.timemates.app.authorization.ui.initial_authorization.mvi.InitialAuthorizationStateMachine.Event
-import org.timemates.app.foundation.mvi.EmptyState
-import org.timemates.app.foundation.mvi.reduce
-import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.TestScope
import org.junit.jupiter.api.Test
+import org.timemates.app.foundation.mvi.reduce
import kotlin.test.assertEquals
class InitialAuthorizationReducerTest {
@@ -20,7 +15,7 @@ class InitialAuthorizationReducerTest {
@Test
fun `reducing event OnStartClicked event should not update the state`() {
//GIVEN
- val state: EmptyState = EmptyState
+ val state = State
val event: Event.OnStartClicked = Event.OnStartClicked
//WHEN
diff --git a/feature/authorization/presentation/src/jvmTestOld/kotlin/org/timemates/app/authorization/ui/new_account_info/mvi/NewAccountInfoReducerTest.kt b/feature/authorization/presentation/src/jvmTestOld/kotlin/org/timemates/app/authorization/ui/new_account_info/mvi/NewAccountInfoReducerTest.kt
new file mode 100644
index 0000000..a110bc0
--- /dev/null
+++ b/feature/authorization/presentation/src/jvmTestOld/kotlin/org/timemates/app/authorization/ui/new_account_info/mvi/NewAccountInfoReducerTest.kt
@@ -0,0 +1,49 @@
+package org.timemates.app.authorization.ui.new_account_info.mvi
+
+import io.mockk.mockk
+import io.mockk.verify
+import org.timemates.sdk.authorization.email.types.value.VerificationHash
+import org.timemates.sdk.common.constructor.createOrThrow
+import kotlinx.coroutines.test.TestScope
+import org.timemates.app.authorization.ui.new_account_info.mvi.NewAccountInfoScreenComponent.Intent
+import org.timemates.app.authorization.ui.new_account_info.mvi.NewAccountInfoScreenComponent.State
+import org.timemates.app.foundation.mvi.reduce
+import org.timemates.app.foundation.random.nextString
+import kotlin.random.Random
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class NewAccountInfoReducerTest {
+ private val verificationHash: VerificationHash = VerificationHash.factory.createOrThrow(Random.nextString(VerificationHash.SIZE))
+ private val sendEffect: (NewAccountInfoScreenComponent.Action) -> Unit = mockk(relaxed = true)
+ private val reducer: NewAccountInfoReducer = NewAccountInfoReducer(verificationHash)
+ private val coroutineScope = TestScope()
+
+ @Test
+ fun `reducing event NextClicked event should not update the state`() {
+ // GIVEN
+ val state = State
+ val event: Intent.NextClicked = Intent.NextClicked
+
+ // WHEN
+ val resultState = reducer.reduce(state, event, coroutineScope, sendEffect)
+
+ // THEN
+ verify { sendEffect(NewAccountInfoScreenComponent.Action.NavigateToAccountConfiguring(verificationHash)) }
+ assertEquals(state, resultState)
+ }
+
+ @Test
+ fun `reducing event OnBackClicked event should not update the state`() {
+ // GIVEN
+ val state = State
+ val event = Intent.OnBackClicked
+
+ // WHEN
+ val resultState = reducer.reduce(state, event, coroutineScope, sendEffect)
+
+ // THEN
+ verify { sendEffect(NewAccountInfoScreenComponent.Action.NavigateToStart) }
+ assertEquals(state, resultState)
+ }
+}
\ No newline at end of file
diff --git a/feature/authorization/presentation/src/jvmTest/kotlin/org/timemates/app/authorization/ui/start/StartAuthorizationMiddlewareTest.kt b/feature/authorization/presentation/src/jvmTestOld/kotlin/org/timemates/app/authorization/ui/start/StartAuthorizationMiddlewareTest.kt
similarity index 59%
rename from feature/authorization/presentation/src/jvmTest/kotlin/org/timemates/app/authorization/ui/start/StartAuthorizationMiddlewareTest.kt
rename to feature/authorization/presentation/src/jvmTestOld/kotlin/org/timemates/app/authorization/ui/start/StartAuthorizationMiddlewareTest.kt
index e33834a..ec51a5c 100644
--- a/feature/authorization/presentation/src/jvmTest/kotlin/org/timemates/app/authorization/ui/start/StartAuthorizationMiddlewareTest.kt
+++ b/feature/authorization/presentation/src/jvmTestOld/kotlin/org/timemates/app/authorization/ui/start/StartAuthorizationMiddlewareTest.kt
@@ -1,31 +1,22 @@
package org.timemates.app.authorization.ui.start
-import io.mockk.every
-import io.mockk.mockk
-import org.timemates.app.authorization.ui.start.mvi.StartAuthorizationMiddleware
-import org.timemates.app.authorization.ui.start.mvi.StartAuthorizationStateMachine.Effect
-import org.timemates.app.authorization.ui.start.mvi.StartAuthorizationStateMachine.State
-import org.timemates.app.foundation.mvi.StateStore
+import org.timemates.sdk.authorization.email.types.value.VerificationHash
+import org.timemates.sdk.common.constructor.createOrThrow
import org.timemates.app.foundation.random.nextString
-import io.timemates.sdk.authorization.email.types.value.VerificationHash
-import io.timemates.sdk.common.constructor.createOrThrow
-import kotlinx.coroutines.flow.MutableStateFlow
import kotlin.random.Random
import kotlin.test.Test
class StartAuthorizationMiddlewareTest {
- private val stateStore: StateStore = mockk()
private val middleware: StartAuthorizationMiddleware = StartAuthorizationMiddleware()
@Test
fun `effects produced by network operations should remove loading status`() {
// GIVEN
val effects = listOf(Effect.TooManyAttempts, Effect.Failure(Exception()))
- every { stateStore.state } returns MutableStateFlow(State(isLoading = true))
// WHEN
- effects.map { effect -> effect to middleware.onEffect(effect, stateStore) }
+ effects.map { effect -> effect to middleware.onEffect(effect, State(isLoading = true)) }
// THEN
.forEach { (effect, state) ->
assert(!state.isLoading) {
@@ -39,13 +30,12 @@ class StartAuthorizationMiddlewareTest {
// GIVEN
val effects = listOf(
Effect.NavigateToConfirmation(
- VerificationHash.createOrThrow(Random.nextString(VerificationHash.SIZE))
+ VerificationHash.factory.createOrThrow(Random.nextString(VerificationHash.SIZE))
)
)
- every { stateStore.state } returns MutableStateFlow(State(isLoading = true))
// WHEN
- effects.map { effect -> effect to middleware.onEffect(effect, stateStore) }
+ effects.map { effect -> effect to middleware.onEffect(effect, State(isLoading = true)) }
// THEN
.forEach { (effect, state) ->
assert(!state.isLoading) {
diff --git a/feature/authorization/presentation/src/jvmTest/kotlin/org/timemates/app/authorization/ui/start/StartAuthorizationReducerTest.kt b/feature/authorization/presentation/src/jvmTestOld/kotlin/org/timemates/app/authorization/ui/start/StartAuthorizationReducerTest.kt
similarity index 90%
rename from feature/authorization/presentation/src/jvmTest/kotlin/org/timemates/app/authorization/ui/start/StartAuthorizationReducerTest.kt
rename to feature/authorization/presentation/src/jvmTestOld/kotlin/org/timemates/app/authorization/ui/start/StartAuthorizationReducerTest.kt
index a799cd5..aa297a7 100644
--- a/feature/authorization/presentation/src/jvmTest/kotlin/org/timemates/app/authorization/ui/start/StartAuthorizationReducerTest.kt
+++ b/feature/authorization/presentation/src/jvmTestOld/kotlin/org/timemates/app/authorization/ui/start/StartAuthorizationReducerTest.kt
@@ -2,14 +2,10 @@ package org.timemates.app.authorization.ui.start
import io.mockk.every
import io.mockk.mockk
-import org.timemates.app.authorization.ui.start.mvi.StartAuthorizationReducer
-import org.timemates.app.authorization.ui.start.mvi.StartAuthorizationStateMachine.Event
-import org.timemates.app.authorization.ui.start.mvi.StartAuthorizationStateMachine.State
+import kotlinx.coroutines.test.TestScope
import org.timemates.app.authorization.usecases.AuthorizeByEmailUseCase
import org.timemates.app.authorization.validation.EmailAddressValidator
import org.timemates.app.foundation.mvi.reduce
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.test.TestScope
import kotlin.test.Test
import kotlin.test.assertEquals
diff --git a/feature/common/README.md b/feature/common/README.md
deleted file mode 100644
index 2d3b602..0000000
--- a/feature/common/README.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# Common feature
-This feature is responsible for basic interfaces, middlewares, etc. for other features to avoid
-unnecessary duplications.
-
-## References
-- See [OnAuthorizationFailedHandler](domain/src/commonMain/kotlin/io/timemates/app/feature/common/handler/OnAuthorizationFailedHandler.kt)
-- See [AuthorizationFailureMiddleware](presentation/src/commonMain/kotlin/io/timemates/app/feature/common/middleware/AuthorizationFailureMiddleware.kt)
\ No newline at end of file
diff --git a/feature/common/presentation/src/commonMain/kotlin/org/timemates/app/feature/common/middleware/AuthorizationFailureMiddleware.kt b/feature/common/presentation/src/commonMain/kotlin/org/timemates/app/feature/common/middleware/AuthorizationFailureMiddleware.kt
deleted file mode 100644
index 108620a..0000000
--- a/feature/common/presentation/src/commonMain/kotlin/org/timemates/app/feature/common/middleware/AuthorizationFailureMiddleware.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-package org.timemates.app.feature.common.middleware
-
-import org.timemates.app.feature.common.handler.OnAuthorizationFailedHandler
-import org.timemates.app.foundation.mvi.Middleware
-import org.timemates.app.foundation.mvi.StateStore
-import org.timemates.app.foundation.mvi.UiEffect
-import org.timemates.app.foundation.mvi.UiState
-import io.timemates.sdk.common.exceptions.UnauthorizedException
-
-/**
- * Middleware class for handling authorization failure effects.
- *
- * @param onAuthorizationFailed The handler for authorization failure events.
- * @param TState The type of UI state.
- * @param TEffect The type of UI effect.
- */
-class AuthorizationFailureMiddleware(
- private val onAuthorizationFailed: OnAuthorizationFailedHandler,
-) : Middleware {
- /**
- * Interface representing an effect indicating an authorization failure.
- * UI effects from different screens should implement this interface
- * to support the AuthorizationFailureMiddleware.
- */
- interface AuthorizationFailureEffect : UiEffect {
- /**
- * The unauthorized exception associated with the authorization failure.
- */
- val exception: UnauthorizedException
- }
-
- /**
- * Handles the [AuthorizationFailureEffect] effect and performs the necessary actions in response to an authorization failure.
- *
- * @param effect The UI effect to handle.
- * @param store The state store to access the current state.
- * @return The updated UI state after handling the effect.
- */
- override fun onEffect(effect: TEffect, state: TState): TState {
- if (effect is AuthorizationFailureEffect)
- onAuthorizationFailed.onFailed(effect.exception)
-
- return state
- }
-}
diff --git a/feature/system/README.md b/feature/splash/README.md
similarity index 100%
rename from feature/system/README.md
rename to feature/splash/README.md
diff --git a/feature/system/adapters/build.gradle.kts b/feature/splash/adapters/build.gradle.kts
similarity index 73%
rename from feature/system/adapters/build.gradle.kts
rename to feature/splash/adapters/build.gradle.kts
index 47b3ddf..d67b13c 100644
--- a/feature/system/adapters/build.gradle.kts
+++ b/feature/splash/adapters/build.gradle.kts
@@ -3,6 +3,6 @@ plugins {
}
dependencies {
- commonMainImplementation(projects.feature.system.domain)
+ commonMainImplementation(projects.feature.splash.domain)
commonMainImplementation(projects.feature.authorization.domain)
}
\ No newline at end of file
diff --git a/feature/system/adapters/src/commonMain/kotlin/org/timemates/app/feature/system/adapters/AuthRepositoryAdapter.kt b/feature/splash/adapters/src/commonMain/kotlin/org/timemates/app/feature/splash/adapters/AuthRepositoryAdapter.kt
similarity index 74%
rename from feature/system/adapters/src/commonMain/kotlin/org/timemates/app/feature/system/adapters/AuthRepositoryAdapter.kt
rename to feature/splash/adapters/src/commonMain/kotlin/org/timemates/app/feature/splash/adapters/AuthRepositoryAdapter.kt
index fc5aa40..b3b153f 100644
--- a/feature/system/adapters/src/commonMain/kotlin/org/timemates/app/feature/system/adapters/AuthRepositoryAdapter.kt
+++ b/feature/splash/adapters/src/commonMain/kotlin/org/timemates/app/feature/splash/adapters/AuthRepositoryAdapter.kt
@@ -1,7 +1,7 @@
-package org.timemates.app.feature.system.adapters
+package org.timemates.app.feature.splash.adapters
import org.timemates.app.authorization.repositories.AuthorizationsRepository
-import org.timemates.app.feature.system.repositories.AuthRepository
+import org.timemates.app.feature.splash.repositories.AuthRepository
class AuthRepositoryAdapter(
private val authorizationsRepository: AuthorizationsRepository
diff --git a/feature/system/dependencies/build.gradle.kts b/feature/splash/dependencies/build.gradle.kts
similarity index 62%
rename from feature/system/dependencies/build.gradle.kts
rename to feature/splash/dependencies/build.gradle.kts
index 0a39a3b..742c2c3 100644
--- a/feature/system/dependencies/build.gradle.kts
+++ b/feature/splash/dependencies/build.gradle.kts
@@ -18,11 +18,11 @@ kotlin {
dependencies {
commonMainImplementation(libs.kotlinx.coroutines)
- commonMainImplementation(projects.feature.system.domain)
- commonMainImplementation(projects.feature.system.presentation)
- commonMainImplementation(projects.feature.system.adapters)
+ commonMainImplementation(projects.feature.splash.domain)
+ commonMainImplementation(projects.feature.splash.presentation)
+ commonMainImplementation(projects.feature.splash.adapters)
commonMainImplementation(projects.feature.authorization.domain)
- commonMainImplementation(projects.feature.common.domain)
+ commonMainImplementation(projects.core.ui)
}
\ No newline at end of file
diff --git a/feature/system/dependencies/src/commonMain/kotlin/org/timemates/app/feature/system/dependencies/SystemDataModule.kt b/feature/splash/dependencies/src/commonMain/kotlin/org/timemates/app/feature/splash/dependencies/SplashDataModule.kt
similarity index 61%
rename from feature/system/dependencies/src/commonMain/kotlin/org/timemates/app/feature/system/dependencies/SystemDataModule.kt
rename to feature/splash/dependencies/src/commonMain/kotlin/org/timemates/app/feature/splash/dependencies/SplashDataModule.kt
index 36ba88e..ce25460 100644
--- a/feature/system/dependencies/src/commonMain/kotlin/org/timemates/app/feature/system/dependencies/SystemDataModule.kt
+++ b/feature/splash/dependencies/src/commonMain/kotlin/org/timemates/app/feature/splash/dependencies/SplashDataModule.kt
@@ -1,13 +1,13 @@
-package org.timemates.app.feature.system.dependencies
+package org.timemates.app.feature.splash.dependencies
import org.timemates.app.authorization.repositories.AuthorizationsRepository
-import org.timemates.app.feature.system.adapters.AuthRepositoryAdapter
-import org.timemates.app.feature.system.repositories.AuthRepository
+import org.timemates.app.feature.splash.adapters.AuthRepositoryAdapter
+import org.timemates.app.feature.splash.repositories.AuthRepository
import org.koin.core.annotation.Module
import org.koin.core.annotation.Singleton
@Module
-class SystemDataModule {
+class SplashDataModule {
@Singleton
fun authRepository(
origin: AuthorizationsRepository,
diff --git a/feature/splash/dependencies/src/commonMain/kotlin/org/timemates/app/feature/splash/dependencies/SplashModule.kt b/feature/splash/dependencies/src/commonMain/kotlin/org/timemates/app/feature/splash/dependencies/SplashModule.kt
new file mode 100644
index 0000000..2183b95
--- /dev/null
+++ b/feature/splash/dependencies/src/commonMain/kotlin/org/timemates/app/feature/splash/dependencies/SplashModule.kt
@@ -0,0 +1,13 @@
+package org.timemates.app.feature.splash.dependencies
+
+import org.koin.core.annotation.Module
+import org.timemates.app.feature.splash.dependencies.screens.StartupModule
+
+@Module(
+ includes = [
+ SplashDataModule::class,
+ // Screen-related
+ StartupModule::class,
+ ]
+)
+class SplashModule
\ No newline at end of file
diff --git a/feature/splash/dependencies/src/commonMain/kotlin/org/timemates/app/feature/splash/dependencies/screens/StartupModule.kt b/feature/splash/dependencies/src/commonMain/kotlin/org/timemates/app/feature/splash/dependencies/screens/StartupModule.kt
new file mode 100644
index 0000000..f860bd0
--- /dev/null
+++ b/feature/splash/dependencies/src/commonMain/kotlin/org/timemates/app/feature/splash/dependencies/screens/StartupModule.kt
@@ -0,0 +1,16 @@
+package org.timemates.app.feature.splash.dependencies.screens
+
+import com.arkivanov.decompose.ComponentContext
+import org.koin.core.annotation.Factory
+import org.koin.core.annotation.Module
+import org.timemates.app.feature.common.startup.mvi.StartupScreenMVIComponent
+import org.timemates.app.feature.splash.dependencies.SplashDataModule
+import org.timemates.app.feature.splash.repositories.AuthRepository
+
+@Module(includes = [SplashDataModule::class])
+class StartupModule {
+ @Factory
+ fun mvi(componentContext: ComponentContext, authRepository: AuthRepository): StartupScreenMVIComponent {
+ return StartupScreenMVIComponent(componentContext, authRepository)
+ }
+}
\ No newline at end of file
diff --git a/feature/common/domain/build.gradle.kts b/feature/splash/domain/build.gradle.kts
similarity index 100%
rename from feature/common/domain/build.gradle.kts
rename to feature/splash/domain/build.gradle.kts
diff --git a/feature/system/domain/src/commonMain/kotlin/org/timemates/app/feature/system/repositories/AuthRepository.kt b/feature/splash/domain/src/commonMain/kotlin/org/timemates/app/feature/splash/repositories/AuthRepository.kt
similarity index 56%
rename from feature/system/domain/src/commonMain/kotlin/org/timemates/app/feature/system/repositories/AuthRepository.kt
rename to feature/splash/domain/src/commonMain/kotlin/org/timemates/app/feature/splash/repositories/AuthRepository.kt
index 8f29428..8825101 100644
--- a/feature/system/domain/src/commonMain/kotlin/org/timemates/app/feature/system/repositories/AuthRepository.kt
+++ b/feature/splash/domain/src/commonMain/kotlin/org/timemates/app/feature/splash/repositories/AuthRepository.kt
@@ -1,4 +1,4 @@
-package org.timemates.app.feature.system.repositories
+package org.timemates.app.feature.splash.repositories
interface AuthRepository {
suspend fun isAuthorized(): Boolean
diff --git a/feature/common/presentation/build.gradle.kts b/feature/splash/presentation/build.gradle.kts
similarity index 58%
rename from feature/common/presentation/build.gradle.kts
rename to feature/splash/presentation/build.gradle.kts
index b4e2c01..ed710f7 100644
--- a/feature/common/presentation/build.gradle.kts
+++ b/feature/splash/presentation/build.gradle.kts
@@ -1,14 +1,12 @@
plugins {
id(libs.plugins.configurations.compose.multiplatform.get().pluginId)
id(libs.plugins.configurations.unit.tests.mockable.get().pluginId)
- alias(libs.plugins.kotlin.parcelize)
+ alias(libs.plugins.kotlinx.serialization)
}
dependencies {
- commonMainApi(projects.styleSystem)
- commonMainApi(projects.foundation.mvi)
- commonMainApi(projects.foundation.mvi.koinCompose)
- commonMainApi(projects.feature.common.domain)
+ commonMainApi(projects.core.styleSystem)
+ commonMainApi(projects.feature.splash.domain)
commonMainImplementation(libs.kotlinx.coroutines)
@@ -16,8 +14,10 @@ dependencies {
commonMainApi(libs.koin.core)
- commonMainApi(projects.localization)
- commonMainApi(projects.localization.compose)
+ commonMainImplementation(projects.core.ui)
+
+ commonMainApi(projects.core.localization)
+ commonMainApi(projects.core.localization.compose)
commonMainApi(libs.decompose)
commonMainApi(libs.decompose.jetbrains.compose)
diff --git a/feature/system/presentation/src/commonMain/kotlin/org/timemates/app/feature/common/startup/StartupScreen.kt b/feature/splash/presentation/src/commonMain/kotlin/org/timemates/app/feature/common/startup/StartupScreen.kt
similarity index 55%
rename from feature/system/presentation/src/commonMain/kotlin/org/timemates/app/feature/common/startup/StartupScreen.kt
rename to feature/splash/presentation/src/commonMain/kotlin/org/timemates/app/feature/common/startup/StartupScreen.kt
index b4e9e12..73de7c9 100644
--- a/feature/system/presentation/src/commonMain/kotlin/org/timemates/app/feature/common/startup/StartupScreen.kt
+++ b/feature/splash/presentation/src/commonMain/kotlin/org/timemates/app/feature/common/startup/StartupScreen.kt
@@ -4,36 +4,29 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import io.github.skeptick.libres.compose.painterResource
-import org.timemates.app.feature.common.startup.mvi.StartupEffect
-import org.timemates.app.feature.common.startup.mvi.StartupEvent
-import org.timemates.app.foundation.mvi.EmptyState
-import org.timemates.app.foundation.mvi.StateMachine
+import org.timemates.app.feature.common.MVI
+import org.timemates.app.feature.common.startup.mvi.StartupScreenMVIComponent.*
import org.timemates.app.style.system.Resources
import org.timemates.app.style.system.theme.AppTheme
-import kotlinx.coroutines.channels.consumeEach
-import kotlinx.coroutines.launch
+import pro.respawn.flowmvi.essenty.compose.subscribe
@Composable
fun StartupScreen(
- stateMachine: StateMachine,
+ mvi: MVI,
navigateToAuth: () -> Unit,
navigateToHome: () -> Unit,
) {
- LaunchedEffect(true) {
- launch { stateMachine.dispatchEvent(StartupEvent.Started) }
- stateMachine.effects.consumeEach { effect ->
- when (effect) {
- StartupEffect.Authorized -> navigateToHome()
- StartupEffect.Unauthorized -> navigateToAuth()
- }
+ @Suppress("UNUSED_VARIABLE")
+ val state = mvi.subscribe { action ->
+ when (action) {
+ Action.GoToAuthorization -> navigateToAuth()
+ Action.GoToMainScreen -> navigateToHome()
}
}
diff --git a/feature/splash/presentation/src/commonMain/kotlin/org/timemates/app/feature/common/startup/mvi/StartupScreenMVIComponent.kt b/feature/splash/presentation/src/commonMain/kotlin/org/timemates/app/feature/common/startup/mvi/StartupScreenMVIComponent.kt
new file mode 100644
index 0000000..806180c
--- /dev/null
+++ b/feature/splash/presentation/src/commonMain/kotlin/org/timemates/app/feature/common/startup/mvi/StartupScreenMVIComponent.kt
@@ -0,0 +1,52 @@
+package org.timemates.app.feature.common.startup.mvi
+
+import com.arkivanov.decompose.ComponentContext
+import org.timemates.app.feature.common.MVI
+import org.timemates.app.feature.common.startup.mvi.StartupScreenMVIComponent.Action
+import org.timemates.app.feature.common.startup.mvi.StartupScreenMVIComponent.Intent
+import org.timemates.app.feature.common.startup.mvi.StartupScreenMVIComponent.State
+import org.timemates.app.feature.splash.repositories.AuthRepository
+import pro.respawn.flowmvi.api.Container
+import pro.respawn.flowmvi.api.MVIAction
+import pro.respawn.flowmvi.api.MVIIntent
+import pro.respawn.flowmvi.api.MVIState
+import pro.respawn.flowmvi.api.Store
+import pro.respawn.flowmvi.essenty.dsl.retainedStore
+import pro.respawn.flowmvi.plugins.init
+
+/**
+ * The global app state machine. Responsible for checking whether user is authorized
+ * and for a new updates (TODO).
+ */
+class StartupScreenMVIComponent(
+ componentContext: ComponentContext,
+ authRepository: AuthRepository,
+) : ComponentContext by componentContext, MVI {
+
+ override val store: Store = retainedStore(initial = State) {
+ init {
+ if (authRepository.isAuthorized())
+ action(Action.GoToMainScreen)
+ else action(Action.GoToAuthorization)
+ }
+ }
+
+ data object State : MVIState
+
+ sealed class Intent : MVIIntent
+
+ sealed class Action : MVIAction {
+ /**
+ * Indicates that user is not authorized and should be moved to the
+ * authorization screen.
+ */
+ data object GoToAuthorization : Action()
+
+ /**
+ * Indicates that user is authorized and should be moved to the
+ * home screen.
+ */
+ data object GoToMainScreen : Action()
+ }
+}
+
diff --git a/feature/system/dependencies/src/commonMain/kotlin/org/timemates/app/feature/system/dependencies/screens/StartupModule.kt b/feature/system/dependencies/src/commonMain/kotlin/org/timemates/app/feature/system/dependencies/screens/StartupModule.kt
deleted file mode 100644
index d59ab72..0000000
--- a/feature/system/dependencies/src/commonMain/kotlin/org/timemates/app/feature/system/dependencies/screens/StartupModule.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package org.timemates.app.feature.system.dependencies.screens
-
-import org.timemates.app.feature.common.startup.mvi.StartupStateMachine
-import org.timemates.app.feature.system.dependencies.SystemDataModule
-import org.timemates.app.feature.system.repositories.AuthRepository
-import org.koin.core.annotation.Factory
-import org.koin.core.annotation.Module
-
-@Module(includes = [SystemDataModule::class])
-class StartupModule {
- @Factory
- fun stateMachine(authRepository: AuthRepository): StartupStateMachine {
- return StartupStateMachine(authRepository)
- }
-}
\ No newline at end of file
diff --git a/feature/system/domain/build.gradle.kts b/feature/system/domain/build.gradle.kts
deleted file mode 100644
index ea4d99b..0000000
--- a/feature/system/domain/build.gradle.kts
+++ /dev/null
@@ -1,13 +0,0 @@
-plugins {
- id(libs.plugins.configurations.multiplatform.library.get().pluginId)
- id(libs.plugins.configurations.unit.tests.mockable.get().pluginId)
-}
-
-dependencies {
- commonMainApi(libs.timemates.sdk)
- commonMainImplementation(libs.kotlinx.coroutines)
-}
-
-android {
- namespace = "org.timemates.app.core"
-}
\ No newline at end of file
diff --git a/feature/system/presentation/build.gradle.kts b/feature/system/presentation/build.gradle.kts
deleted file mode 100644
index 41f52f3..0000000
--- a/feature/system/presentation/build.gradle.kts
+++ /dev/null
@@ -1,28 +0,0 @@
-plugins {
- id(libs.plugins.configurations.compose.multiplatform.get().pluginId)
- id(libs.plugins.configurations.unit.tests.mockable.get().pluginId)
- alias(libs.plugins.kotlin.parcelize)
-}
-
-dependencies {
- commonMainApi(projects.styleSystem)
- commonMainApi(projects.foundation.mvi)
- commonMainApi(projects.foundation.mvi.koinCompose)
- commonMainApi(projects.feature.system.domain)
-
- commonMainImplementation(libs.kotlinx.coroutines)
-
- commonMainApi(compose.materialIconsExtended)
-
- commonMainApi(libs.koin.core)
-
- commonMainApi(projects.localization)
- commonMainApi(projects.localization.compose)
-
- commonMainApi(libs.decompose)
- commonMainApi(libs.decompose.jetbrains.compose)
-}
-
-android {
- namespace = "org.timemates.app.core.ui"
-}
\ No newline at end of file
diff --git a/feature/system/presentation/src/commonMain/kotlin/org/timemates/app/feature/common/startup/mvi/StartupStateMachine.kt b/feature/system/presentation/src/commonMain/kotlin/org/timemates/app/feature/common/startup/mvi/StartupStateMachine.kt
deleted file mode 100644
index 038d136..0000000
--- a/feature/system/presentation/src/commonMain/kotlin/org/timemates/app/feature/common/startup/mvi/StartupStateMachine.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-package org.timemates.app.feature.common.startup.mvi
-
-import org.timemates.app.feature.system.repositories.AuthRepository
-import org.timemates.app.foundation.mvi.EmptyState
-import org.timemates.app.foundation.mvi.ReducerScope
-import org.timemates.app.foundation.mvi.StateMachine
-import org.timemates.app.foundation.mvi.UiEffect
-import org.timemates.app.foundation.mvi.UiEvent
-import kotlinx.coroutines.launch
-import org.timemates.app.foundation.mvi.Reducer as MviReducer
-
-/**
- * The global app state machine. Responsible for checking whether user is authorized
- * and for a new updates (TODO).
- */
-class StartupStateMachine(
- authRepository: AuthRepository,
-) : StateMachine(
- initState = EmptyState,
- reducer = Reducer(authRepository),
-) {
- class Reducer(
- private val authRepository: AuthRepository,
- ) : MviReducer {
- override fun ReducerScope.reduce(
- state: EmptyState,
- event: StartupEvent,
- ): EmptyState {
- return when (event) {
- StartupEvent.Started -> {
- machineScope.launch {
- if (authRepository.isAuthorized())
- sendEffect(StartupEffect.Authorized)
- else sendEffect(StartupEffect.Unauthorized)
- }
-
- state
- }
- }
- }
- }
-}
-
-sealed class StartupEvent : UiEvent {
- /**
- * Indicates that app is started.
- */
- data object Started : StartupEvent()
-}
-
-sealed class StartupEffect : UiEffect {
- /**
- * Indicates that user is not authorized and should be moved to the
- * authorization screen.
- */
- data object Unauthorized : StartupEffect()
-
- /**
- * Indicates that user is authorized and should be moved to the
- * home screen.
- */
- data object Authorized : StartupEffect()
-}
\ No newline at end of file
diff --git a/feature/timers/data/src/commonMain/kotlin/org/timemates/app/timers/data/TimersRepository.kt b/feature/timers/data/src/commonMain/kotlin/org/timemates/app/timers/data/TimersRepository.kt
index 3754cc3..c9ee596 100644
--- a/feature/timers/data/src/commonMain/kotlin/org/timemates/app/timers/data/TimersRepository.kt
+++ b/feature/timers/data/src/commonMain/kotlin/org/timemates/app/timers/data/TimersRepository.kt
@@ -1,24 +1,31 @@
package org.timemates.app.timers.data
-import io.timemates.sdk.common.pagination.PageToken
-import io.timemates.sdk.common.pagination.PagesIterator
-import io.timemates.sdk.common.types.Empty
-import io.timemates.sdk.common.types.value.Count
-import io.timemates.sdk.timers.TimersApi
-import io.timemates.sdk.timers.getUserTimersPages
-import io.timemates.sdk.timers.members.TimerMembersApi
-import io.timemates.sdk.timers.members.invites.TimerInvitesApi
-import io.timemates.sdk.timers.members.invites.getInvitesPages
-import io.timemates.sdk.timers.members.invites.types.Invite
-import io.timemates.sdk.timers.members.invites.types.value.InviteCode
-import io.timemates.sdk.timers.sessions.TimersSessionsApi
-import io.timemates.sdk.timers.types.Timer
-import io.timemates.sdk.timers.types.TimerSettings
-import io.timemates.sdk.timers.types.value.TimerDescription
-import io.timemates.sdk.timers.types.value.TimerId
-import io.timemates.sdk.timers.types.value.TimerName
-import io.timemates.sdk.users.profile.types.value.UserId
+import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+import org.timemates.app.users.repositories.TimersRepository.TimerUpdateAction
+import org.timemates.sdk.common.pagination.PageToken
+import org.timemates.sdk.common.pagination.PagesIterator
+import org.timemates.sdk.common.types.Empty
+import org.timemates.sdk.common.types.value.Count
+import org.timemates.sdk.timers.TimersApi
+import org.timemates.sdk.timers.getUserTimersPages
+import org.timemates.sdk.timers.members.TimerMembersApi
+import org.timemates.sdk.timers.members.invites.TimerInvitesApi
+import org.timemates.sdk.timers.members.invites.getInvitesPages
+import org.timemates.sdk.timers.members.invites.types.Invite
+import org.timemates.sdk.timers.members.invites.types.value.InviteCode
+import org.timemates.sdk.timers.sessions.TimersSessionsApi
+import org.timemates.sdk.timers.types.Timer
+import org.timemates.sdk.timers.types.TimerSettings
+import org.timemates.sdk.timers.types.value.TimerDescription
+import org.timemates.sdk.timers.types.value.TimerId
+import org.timemates.sdk.timers.types.value.TimerName
+import org.timemates.sdk.users.profile.types.value.UserId
import org.timemates.app.users.repositories.TimersRepository as TimersRepositoryContract
class TimersRepository(
@@ -27,10 +34,16 @@ class TimersRepository(
private val timersSessionsApi: TimersSessionsApi,
private val timerMembersApi: TimerMembersApi,
) : TimersRepositoryContract {
+ private val timerUpdates = MutableSharedFlow(replay = Int.MAX_VALUE, extraBufferCapacity = Int.MAX_VALUE)
+
override suspend fun getUserTimers(pageToken: PageToken?): PagesIterator {
return timersApi.getUserTimersPages(pageToken)
}
+ override fun getTimersUpdates(): SharedFlow {
+ return timerUpdates
+ }
+
override suspend fun getTimer(id: TimerId): Result {
return timersApi.getTimer(id)
}
@@ -48,7 +61,9 @@ class TimersRepository(
}
override suspend fun createTimer(name: TimerName, description: TimerDescription, settings: TimerSettings): Result {
- return timersApi.createTimer(name, description, settings)
+ return timersApi.createTimer(name, description, settings).onSuccess {
+ timerUpdates.emit(TimerUpdateAction.Added(getTimer(it).getOrNull() ?: return@onSuccess))
+ }
}
override suspend fun kickMember(timerId: TimerId, userId: UserId): Result {
@@ -60,10 +75,19 @@ class TimersRepository(
}
override suspend fun removeTimer(timerId: TimerId): Result {
- return timersApi.removeTimer(timerId)
+ return timersApi.removeTimer(timerId).onSuccess {
+ timerUpdates.emit(TimerUpdateAction.Deleted(timerId))
+ }
}
override suspend fun editTimer(timerId: TimerId, newName: TimerName?, newDescription: TimerDescription?, settings: TimerSettings.Patch?): Result {
- return timersApi.editTimer(timerId, newName, newDescription, settings)
+ return timersApi.editTimer(timerId, newName, newDescription, settings).onSuccess {
+ timerUpdates.emit(
+ TimerUpdateAction.Updated(
+ // todo get from the local storage
+ getTimer(timerId).getOrNull() ?: return@onSuccess
+ )
+ )
+ }
}
}
\ No newline at end of file
diff --git a/feature/timers/dependencies/build.gradle.kts b/feature/timers/dependencies/build.gradle.kts
index a87eb32..acc55b1 100644
--- a/feature/timers/dependencies/build.gradle.kts
+++ b/feature/timers/dependencies/build.gradle.kts
@@ -22,5 +22,5 @@ dependencies {
commonMainImplementation(projects.feature.timers.domain)
commonMainImplementation(projects.feature.timers.data)
commonMainImplementation(projects.feature.timers.presentation)
- commonMainImplementation(projects.feature.common.domain)
+ commonMainImplementation(projects.core.ui)
}
\ No newline at end of file
diff --git a/feature/timers/dependencies/src/commonMain/kotlin/org/timemates/app/timers/dependencies/TimersDataModule.kt b/feature/timers/dependencies/src/commonMain/kotlin/org/timemates/app/timers/dependencies/TimersDataModule.kt
index 25797e2..f974d67 100644
--- a/feature/timers/dependencies/src/commonMain/kotlin/org/timemates/app/timers/dependencies/TimersDataModule.kt
+++ b/feature/timers/dependencies/src/commonMain/kotlin/org/timemates/app/timers/dependencies/TimersDataModule.kt
@@ -1,22 +1,22 @@
package org.timemates.app.timers.dependencies
-import org.timemates.app.users.repositories.TimersRepository
-import io.timemates.sdk.common.engine.TimeMatesRequestsEngine
-import io.timemates.sdk.common.providers.AccessHashProvider
-import io.timemates.sdk.timers.TimersApi
-import org.koin.core.annotation.Factory
import org.koin.core.annotation.Module
+import org.koin.core.annotation.Singleton
+import org.timemates.app.users.repositories.TimersRepository
+import org.timemates.sdk.common.engine.TimeMatesRequestsEngine
+import org.timemates.sdk.common.providers.AccessHashProvider
+import org.timemates.sdk.timers.TimersApi
import org.timemates.app.timers.data.TimersRepository as TimersRepositoryImpl
@Module
class TimersDataModule {
- @Factory
+ @Singleton
fun timersApi(
- grpcEngine: TimeMatesRequestsEngine,
+ engine: TimeMatesRequestsEngine,
tokenProvider: AccessHashProvider,
- ): TimersApi = TimersApi(grpcEngine, tokenProvider)
+ ): TimersApi = TimersApi(engine, tokenProvider)
- @Factory
+ @Singleton
fun timersRepository(
timersApi: TimersApi,
): TimersRepository = TimersRepositoryImpl(
diff --git a/feature/timers/dependencies/src/commonMain/kotlin/org/timemates/app/timers/dependencies/TimersModule.kt b/feature/timers/dependencies/src/commonMain/kotlin/org/timemates/app/timers/dependencies/TimersModule.kt
new file mode 100644
index 0000000..9df426f
--- /dev/null
+++ b/feature/timers/dependencies/src/commonMain/kotlin/org/timemates/app/timers/dependencies/TimersModule.kt
@@ -0,0 +1,17 @@
+package org.timemates.app.timers.dependencies
+
+import org.koin.core.annotation.Module
+import org.timemates.app.timers.dependencies.screens.TimerCreationModule
+import org.timemates.app.timers.dependencies.screens.TimerSettingsModule
+import org.timemates.app.timers.dependencies.screens.TimersListModule
+
+@Module(
+ includes = [
+ TimersDataModule::class,
+ // Screen-related
+ TimerCreationModule::class,
+ TimerSettingsModule::class,
+ TimersListModule::class,
+ ]
+)
+class TimersModule
\ No newline at end of file
diff --git a/feature/timers/dependencies/src/commonMain/kotlin/org/timemates/app/timers/dependencies/screens/TimerCreationModule.kt b/feature/timers/dependencies/src/commonMain/kotlin/org/timemates/app/timers/dependencies/screens/TimerCreationModule.kt
index ac6a9ae..e993939 100644
--- a/feature/timers/dependencies/src/commonMain/kotlin/org/timemates/app/timers/dependencies/screens/TimerCreationModule.kt
+++ b/feature/timers/dependencies/src/commonMain/kotlin/org/timemates/app/timers/dependencies/screens/TimerCreationModule.kt
@@ -1,50 +1,29 @@
package org.timemates.app.timers.dependencies.screens
+import com.arkivanov.decompose.ComponentContext
+import org.koin.core.annotation.Factory
+import org.koin.core.annotation.Module
import org.timemates.app.timers.dependencies.TimersDataModule
-import org.timemates.app.timers.ui.timer_creation.mvi.TimerCreationMiddleware
-import org.timemates.app.timers.ui.timer_creation.mvi.TimerCreationReducer
-import org.timemates.app.timers.ui.timer_creation.mvi.TimerCreationStateMachine
+import org.timemates.app.timers.ui.timer_creation.mvi.TimerCreationScreenComponent
import org.timemates.app.users.repositories.TimersRepository
import org.timemates.app.users.usecases.TimerCreationUseCase
-import org.timemates.app.users.validation.TimerDescriptionValidator
-import org.timemates.app.users.validation.TimerNameValidator
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import org.koin.core.annotation.Factory
-import org.koin.core.annotation.Module
-import org.koin.core.annotation.Singleton
@Module(includes = [TimersDataModule::class])
class TimerCreationModule {
- @Singleton
- fun timerCreationMiddleware(): TimerCreationMiddleware = TimerCreationMiddleware()
-
@Factory
fun timerCreationUseCase(
timersRepository: TimersRepository,
): TimerCreationUseCase = TimerCreationUseCase(timersRepository)
- @Singleton
- fun timerNameValidator(): TimerNameValidator = TimerNameValidator()
-
- @Singleton
- fun timerDescriptionValidator(): TimerDescriptionValidator = TimerDescriptionValidator()
-
@Factory
fun stateMachine(
+ componentContext: ComponentContext,
timerCreationUseCase: TimerCreationUseCase,
- timerNameValidator: TimerNameValidator,
- timerDescriptionValidator: TimerDescriptionValidator,
- middleware: TimerCreationMiddleware,
- ): TimerCreationStateMachine {
- return TimerCreationStateMachine(
- reducer = TimerCreationReducer(
- timerCreationUseCase = timerCreationUseCase,
- timerNameValidator = timerNameValidator,
- timerDescriptionValidator = timerDescriptionValidator,
- ),
- middleware = middleware,
+ ): TimerCreationScreenComponent {
+ return TimerCreationScreenComponent(
+ componentContext = componentContext,
+ timerCreationUseCase = timerCreationUseCase,
)
}
}
\ No newline at end of file
diff --git a/feature/timers/dependencies/src/commonMain/kotlin/org/timemates/app/timers/dependencies/screens/TimerSettingsModule.kt b/feature/timers/dependencies/src/commonMain/kotlin/org/timemates/app/timers/dependencies/screens/TimerSettingsModule.kt
index 8abacf7..996ff7c 100644
--- a/feature/timers/dependencies/src/commonMain/kotlin/org/timemates/app/timers/dependencies/screens/TimerSettingsModule.kt
+++ b/feature/timers/dependencies/src/commonMain/kotlin/org/timemates/app/timers/dependencies/screens/TimerSettingsModule.kt
@@ -1,53 +1,32 @@
package org.timemates.app.timers.dependencies.screens
+import com.arkivanov.decompose.ComponentContext
+import org.koin.core.annotation.Factory
+import org.koin.core.annotation.Module
import org.timemates.app.timers.dependencies.TimersDataModule
-import org.timemates.app.timers.ui.settings.mvi.TimerSettingsMiddleware
-import org.timemates.app.timers.ui.settings.mvi.TimerSettingsReducer
-import org.timemates.app.timers.ui.settings.mvi.TimerSettingsStateMachine
+import org.timemates.app.timers.ui.settings.mvi.TimerSettingsScreenComponent
import org.timemates.app.users.repositories.TimersRepository
import org.timemates.app.users.usecases.TimerSettingsUseCase
-import org.timemates.app.users.validation.TimerDescriptionValidator
-import org.timemates.app.users.validation.TimerNameValidator
-import io.timemates.sdk.timers.types.value.TimerId
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import org.koin.core.annotation.Factory
-import org.koin.core.annotation.Module
-import org.koin.core.annotation.Singleton
+import org.timemates.sdk.timers.types.value.TimerId
@Module(includes = [TimersDataModule::class])
class TimerSettingsModule {
- @Singleton
- fun timerSettingsMiddleware(): TimerSettingsMiddleware = TimerSettingsMiddleware()
-
@Factory
fun timerSettingsUseCase(
timersRepository: TimersRepository,
): TimerSettingsUseCase = TimerSettingsUseCase(timersRepository)
- @Singleton
- fun timerNameValidator(): TimerNameValidator = TimerNameValidator()
-
- @Singleton
- fun timerDescriptionValidator(): TimerDescriptionValidator = TimerDescriptionValidator()
-
@Factory
fun stateMachine(
+ componentContext: ComponentContext,
timerId: TimerId,
timerSettingsUseCase: TimerSettingsUseCase,
- timerNameValidator: TimerNameValidator,
- timerDescriptionValidator: TimerDescriptionValidator,
- middleware: TimerSettingsMiddleware,
- ): TimerSettingsStateMachine {
- return TimerSettingsStateMachine(
- reducer = TimerSettingsReducer(
- timerId = timerId,
- timerSettingsUseCase = timerSettingsUseCase,
- timerNameValidator = timerNameValidator,
- timerDescriptionValidator = timerDescriptionValidator,
- ),
- middleware = middleware,
+ ): TimerSettingsScreenComponent {
+ return TimerSettingsScreenComponent(
+ componentContext = componentContext,
+ timerId = timerId,
+ timerSettingsUseCase = timerSettingsUseCase,
)
}
}
diff --git a/feature/timers/dependencies/src/commonMain/kotlin/org/timemates/app/timers/dependencies/screens/TimersListModule.kt b/feature/timers/dependencies/src/commonMain/kotlin/org/timemates/app/timers/dependencies/screens/TimersListModule.kt
index 4850836..1d6b31e 100644
--- a/feature/timers/dependencies/src/commonMain/kotlin/org/timemates/app/timers/dependencies/screens/TimersListModule.kt
+++ b/feature/timers/dependencies/src/commonMain/kotlin/org/timemates/app/timers/dependencies/screens/TimersListModule.kt
@@ -1,23 +1,16 @@
package org.timemates.app.timers.dependencies.screens
+import com.arkivanov.decompose.ComponentContext
+import org.koin.core.annotation.Factory
+import org.koin.core.annotation.Module
import org.timemates.app.timers.dependencies.TimersDataModule
-import org.timemates.app.timers.ui.timers_list.mvi.TimersListMiddleware
-import org.timemates.app.timers.ui.timers_list.mvi.TimersListReducer
-import org.timemates.app.timers.ui.timers_list.mvi.TimersListStateMachine
+import org.timemates.app.timers.ui.timers_list.mvi.TimersListScreenComponent
import org.timemates.app.users.repositories.TimersRepository
import org.timemates.app.users.usecases.GetUserTimersUseCase
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import org.koin.core.annotation.Factory
-import org.koin.core.annotation.Module
-import org.koin.core.annotation.Singleton
@Module(includes = [TimersDataModule::class])
class TimersListModule {
- @Singleton
- fun timersListMiddleware(): TimersListMiddleware = TimersListMiddleware()
-
@Factory
fun getUserTimersUseCase(
timersRepository: TimersRepository,
@@ -25,14 +18,14 @@ class TimersListModule {
@Factory
fun stateMachine(
+ componentContext: ComponentContext,
getUserTimersUseCase: GetUserTimersUseCase,
- timersListMiddleware: TimersListMiddleware,
- ): TimersListStateMachine {
- return TimersListStateMachine(
- reducer = TimersListReducer(
- getUserTimersUseCase = getUserTimersUseCase,
- ),
- middleware = timersListMiddleware,
+ timersRepository: TimersRepository,
+ ): TimersListScreenComponent {
+ return TimersListScreenComponent(
+ componentContext = componentContext,
+ getUserTimersUseCase = getUserTimersUseCase,
+ timersRepository = timersRepository,
)
}
}
\ No newline at end of file
diff --git a/feature/timers/domain/src/commonMain/kotlin/org/timemates/app/users/repositories/TimersRepository.kt b/feature/timers/domain/src/commonMain/kotlin/org/timemates/app/users/repositories/TimersRepository.kt
index 81e7185..08519a9 100644
--- a/feature/timers/domain/src/commonMain/kotlin/org/timemates/app/users/repositories/TimersRepository.kt
+++ b/feature/timers/domain/src/commonMain/kotlin/org/timemates/app/users/repositories/TimersRepository.kt
@@ -1,22 +1,25 @@
package org.timemates.app.users.repositories
-import io.timemates.sdk.common.pagination.PageToken
-import io.timemates.sdk.common.pagination.PagesIterator
-import io.timemates.sdk.common.types.Empty
-import io.timemates.sdk.common.types.value.Count
-import io.timemates.sdk.timers.members.invites.types.Invite
-import io.timemates.sdk.timers.members.invites.types.value.InviteCode
-import io.timemates.sdk.timers.types.Timer
-import io.timemates.sdk.timers.types.TimerSettings
-import io.timemates.sdk.timers.types.value.TimerDescription
-import io.timemates.sdk.timers.types.value.TimerId
-import io.timemates.sdk.timers.types.value.TimerName
-import io.timemates.sdk.users.profile.types.value.UserId
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharedFlow
+import org.timemates.sdk.common.pagination.PageToken
+import org.timemates.sdk.common.pagination.PagesIterator
+import org.timemates.sdk.common.types.Empty
+import org.timemates.sdk.common.types.value.Count
+import org.timemates.sdk.timers.members.invites.types.Invite
+import org.timemates.sdk.timers.members.invites.types.value.InviteCode
+import org.timemates.sdk.timers.types.Timer
+import org.timemates.sdk.timers.types.TimerSettings
+import org.timemates.sdk.timers.types.value.TimerDescription
+import org.timemates.sdk.timers.types.value.TimerId
+import org.timemates.sdk.timers.types.value.TimerName
+import org.timemates.sdk.users.profile.types.value.UserId
interface TimersRepository {
suspend fun getUserTimers(pageToken: PageToken? = null): PagesIterator
+ fun getTimersUpdates(): SharedFlow
+
suspend fun getTimer(id: TimerId): Result
suspend fun getTimerState(id: TimerId): Result>
@@ -43,4 +46,12 @@ interface TimersRepository {
newDescription: TimerDescription? = null,
settings: TimerSettings.Patch? = null,
): Result
+
+ sealed interface TimerUpdateAction {
+ data class Deleted(val timerId: TimerId) : TimerUpdateAction
+
+ data class Updated(val timer: Timer) : TimerUpdateAction
+
+ data class Added(val timer: Timer) : TimerUpdateAction
+ }
}
\ No newline at end of file
diff --git a/feature/timers/domain/src/commonMain/kotlin/org/timemates/app/users/usecases/GetTimerUseCase.kt b/feature/timers/domain/src/commonMain/kotlin/org/timemates/app/users/usecases/GetTimerUseCase.kt
index 9d29146..00a3b3c 100644
--- a/feature/timers/domain/src/commonMain/kotlin/org/timemates/app/users/usecases/GetTimerUseCase.kt
+++ b/feature/timers/domain/src/commonMain/kotlin/org/timemates/app/users/usecases/GetTimerUseCase.kt
@@ -1,8 +1,8 @@
package org.timemates.app.users.usecases
import org.timemates.app.users.repositories.TimersRepository
-import io.timemates.sdk.timers.types.Timer
-import io.timemates.sdk.timers.types.value.TimerId
+import org.timemates.sdk.timers.types.Timer
+import org.timemates.sdk.timers.types.value.TimerId
class GetTimerUseCase(
private val timers: TimersRepository,
@@ -16,7 +16,7 @@ class GetTimerUseCase(
sealed class Result {
data class Success(val timer: Timer) : Result()
- object NotFound : Result()
+ data object NotFound : Result()
data class Failure(val exception: Throwable) : Result()
}
diff --git a/feature/timers/domain/src/commonMain/kotlin/org/timemates/app/users/usecases/GetUserTimersUseCase.kt b/feature/timers/domain/src/commonMain/kotlin/org/timemates/app/users/usecases/GetUserTimersUseCase.kt
index 18a9f54..c7326fe 100644
--- a/feature/timers/domain/src/commonMain/kotlin/org/timemates/app/users/usecases/GetUserTimersUseCase.kt
+++ b/feature/timers/domain/src/commonMain/kotlin/org/timemates/app/users/usecases/GetUserTimersUseCase.kt
@@ -1,8 +1,8 @@
package org.timemates.app.users.usecases
import org.timemates.app.users.repositories.TimersRepository
-import io.timemates.sdk.common.pagination.PagesIterator
-import io.timemates.sdk.timers.types.Timer
+import org.timemates.sdk.common.pagination.PagesIterator
+import org.timemates.sdk.timers.types.Timer
class GetUserTimersUseCase(
private val timers: TimersRepository,
diff --git a/feature/timers/domain/src/commonMain/kotlin/org/timemates/app/users/usecases/TimerCreationUseCase.kt b/feature/timers/domain/src/commonMain/kotlin/org/timemates/app/users/usecases/TimerCreationUseCase.kt
index 18d7621..636b9f6 100644
--- a/feature/timers/domain/src/commonMain/kotlin/org/timemates/app/users/usecases/TimerCreationUseCase.kt
+++ b/feature/timers/domain/src/commonMain/kotlin/org/timemates/app/users/usecases/TimerCreationUseCase.kt
@@ -1,9 +1,9 @@
package org.timemates.app.users.usecases
import org.timemates.app.users.repositories.TimersRepository
-import io.timemates.sdk.timers.types.TimerSettings
-import io.timemates.sdk.timers.types.value.TimerDescription
-import io.timemates.sdk.timers.types.value.TimerName
+import org.timemates.sdk.timers.types.TimerSettings
+import org.timemates.sdk.timers.types.value.TimerDescription
+import org.timemates.sdk.timers.types.value.TimerName
class TimerCreationUseCase(
private val timersRepository: TimersRepository,
diff --git a/feature/timers/domain/src/commonMain/kotlin/org/timemates/app/users/usecases/TimerSettingsUseCase.kt b/feature/timers/domain/src/commonMain/kotlin/org/timemates/app/users/usecases/TimerSettingsUseCase.kt
index 57b73f5..d997174 100644
--- a/feature/timers/domain/src/commonMain/kotlin/org/timemates/app/users/usecases/TimerSettingsUseCase.kt
+++ b/feature/timers/domain/src/commonMain/kotlin/org/timemates/app/users/usecases/TimerSettingsUseCase.kt
@@ -1,10 +1,10 @@
package org.timemates.app.users.usecases
import org.timemates.app.users.repositories.TimersRepository
-import io.timemates.sdk.timers.types.TimerSettings
-import io.timemates.sdk.timers.types.value.TimerDescription
-import io.timemates.sdk.timers.types.value.TimerId
-import io.timemates.sdk.timers.types.value.TimerName
+import org.timemates.sdk.timers.types.TimerSettings
+import org.timemates.sdk.timers.types.value.TimerDescription
+import org.timemates.sdk.timers.types.value.TimerId
+import org.timemates.sdk.timers.types.value.TimerName
class TimerSettingsUseCase(
private val timers: TimersRepository,
diff --git a/feature/timers/domain/src/commonMain/kotlin/org/timemates/app/users/validation/TimerDescriptionValidator.kt b/feature/timers/domain/src/commonMain/kotlin/org/timemates/app/users/validation/TimerDescriptionValidator.kt
deleted file mode 100644
index 824d995..0000000
--- a/feature/timers/domain/src/commonMain/kotlin/org/timemates/app/users/validation/TimerDescriptionValidator.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-package org.timemates.app.users.validation
-
-import org.timemates.app.foundation.validation.Validator
-import org.timemates.app.foundation.validation.unknownValidationFailure
-import org.timemates.app.users.validation.TimerDescriptionValidator.Result
-import io.timemates.sdk.common.constructor.CreationFailure
-import io.timemates.sdk.timers.types.value.TimerDescription
-
-class TimerDescriptionValidator : Validator {
-
- override fun validate(input: String): Result {
- return TimerDescription.create(input)
- .map { Result.Success }
- .getOrElse { throwable ->
- when (throwable) {
- is CreationFailure.SizeRangeFailure -> Result.SizeViolation
- else -> unknownValidationFailure(throwable)
- }
- }
- }
-
- sealed class Result {
- object Success : Result()
-
- object SizeViolation : Result()
- }
-}
\ No newline at end of file
diff --git a/feature/timers/domain/src/commonMain/kotlin/org/timemates/app/users/validation/TimerNameValidator.kt b/feature/timers/domain/src/commonMain/kotlin/org/timemates/app/users/validation/TimerNameValidator.kt
deleted file mode 100644
index 64bb97d..0000000
--- a/feature/timers/domain/src/commonMain/kotlin/org/timemates/app/users/validation/TimerNameValidator.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-package org.timemates.app.users.validation
-
-import org.timemates.app.foundation.validation.Validator
-import org.timemates.app.foundation.validation.unknownValidationFailure
-import org.timemates.app.users.validation.TimerNameValidator.Result
-import io.timemates.sdk.common.constructor.CreationFailure
-import io.timemates.sdk.timers.types.value.TimerName
-
-class TimerNameValidator : Validator {
-
- override fun validate(input: String): Result {
- return TimerName.create(input)
- .map { Result.Success }
- .getOrElse { throwable ->
- when (throwable) {
- is CreationFailure.SizeRangeFailure -> Result.SizeViolation
- else -> unknownValidationFailure(throwable)
- }
- }
- }
-
- sealed class Result {
- object Success : Result()
-
- object SizeViolation : Result()
- }
-}
\ No newline at end of file
diff --git a/feature/timers/presentation/build.gradle.kts b/feature/timers/presentation/build.gradle.kts
index 5747b4c..fa0d303 100644
--- a/feature/timers/presentation/build.gradle.kts
+++ b/feature/timers/presentation/build.gradle.kts
@@ -1,13 +1,13 @@
plugins {
id(libs.plugins.configurations.compose.multiplatform.get().pluginId)
id(libs.plugins.configurations.unit.tests.mockable.get().pluginId)
+ alias(libs.plugins.kotlinx.serialization)
}
dependencies {
- commonMainImplementation(projects.feature.common.presentation)
+ commonMainImplementation(projects.core.ui)
commonMainImplementation(libs.timemates.sdk)
- commonMainApi(projects.foundation.mvi)
- commonMainImplementation(projects.styleSystem)
+ commonMainImplementation(projects.core.styleSystem)
commonMainImplementation(projects.feature.timers.domain)
commonTestImplementation(projects.foundation.random)
@@ -16,5 +16,6 @@ dependencies {
commonMainImplementation(libs.moko.resources)
commonMainImplementation(libs.kotlinx.datetime)
+ commonMainImplementation(projects.foundation.time)
}
diff --git a/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/ShimmerTimerItem.kt b/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/ShimmerTimerItem.kt
index 49b90b6..62de1eb 100644
--- a/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/ShimmerTimerItem.kt
+++ b/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/ShimmerTimerItem.kt
@@ -34,17 +34,17 @@ import androidx.compose.ui.unit.dp
import org.timemates.app.style.system.theme.AppTheme
@Composable
-fun PlaceholderTimerItem(){
+fun PlaceholderTimerItem() {
OutlinedCard(
modifier = Modifier.fillMaxWidth(),
border = BorderStroke(1.dp, AppTheme.colors.secondary),
colors = CardDefaults.outlinedCardColors(containerColor = AppTheme.colors.background),
- ) {
+ ) {
Row(
modifier = Modifier.padding(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
- Column{
+ Column {
Box(
modifier = Modifier
.fillMaxWidth(0.4f)
diff --git a/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/TimerItem.kt b/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/TimerItem.kt
index f545cef..b57b569 100644
--- a/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/TimerItem.kt
+++ b/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/TimerItem.kt
@@ -11,7 +11,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.ArrowForward
+import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@@ -25,19 +25,14 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
-import org.timemates.app.localization.compose.LocalStrings
-import org.timemates.app.style.system.theme.AppTheme
-import io.timemates.sdk.timers.types.Timer
-import io.timemates.sdk.timers.types.Timer.State.ConfirmationWaiting
-import io.timemates.sdk.timers.types.Timer.State.Inactive
-import io.timemates.sdk.timers.types.Timer.State.Paused
-import io.timemates.sdk.timers.types.Timer.State.Rest
-import io.timemates.sdk.timers.types.Timer.State.Running
-import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.datetime.TimeZone
import kotlinx.datetime.minus
import kotlinx.datetime.toLocalDateTime
+import org.timemates.app.feature.common.providable.LocalTimeProvider
+import org.timemates.app.localization.compose.LocalStrings
+import org.timemates.app.style.system.theme.AppTheme
+import org.timemates.sdk.timers.types.Timer
@Composable
fun TimerItem(
@@ -69,7 +64,7 @@ fun TimerItem(
)
OnlineIndicator(
modifier = Modifier.padding(3.dp),
- isOnline = timer.state is Running,
+ isOnline = timer.state is Timer.State.Running,
)
}
Text(
@@ -82,7 +77,7 @@ fun TimerItem(
modifier = Modifier.padding(12.dp),
) {
Icon(
- imageVector = Icons.Filled.ArrowForward,
+ imageVector = Icons.AutoMirrored.Filled.ArrowForward,
contentDescription = "Navigate To Timer",
modifier = Modifier
.align(Alignment.CenterEnd),
@@ -96,16 +91,21 @@ fun TimerItem(
@Stable
@Composable
private fun timerItemSubtext(timer: Timer): String {
- return when(timer.state) {
- is Running, is Rest -> LocalStrings.current.runningTimerDescription(timer.membersCount.int)
- is Paused, is Inactive -> LocalStrings.current.inactiveTimerDescription(daysSinceInactive(timer.state.publishTime))
- is ConfirmationWaiting -> LocalStrings.current.confirmationWaitingTimerDescription
+ return when (timer.state) {
+ is Timer.State.Running, is Timer.State.Rest ->
+ LocalStrings.current.runningTimerDescription(timer.membersCount.int)
+
+ is Timer.State.Paused, is Timer.State.Inactive ->
+ LocalStrings.current.inactiveTimerDescription(daysSinceInactive(timer.state.publishTime))
+
+ is Timer.State.ConfirmationWaiting -> LocalStrings.current.confirmationWaitingTimerDescription
}
}
+@Composable
@Stable
private fun daysSinceInactive(pausedInstant: Instant): Int {
- val currentInstant = Clock.System.now()
+ val currentInstant = LocalTimeProvider.current.provide()
return (currentInstant.toLocalDateTime(TimeZone.UTC).date.minus(pausedInstant.toLocalDateTime(TimeZone.UTC).date)).days
}
diff --git a/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/settings/TimerSettingsScreen.kt b/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/settings/TimerSettingsScreen.kt
index 26ba8aa..38ce60a 100644
--- a/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/settings/TimerSettingsScreen.kt
+++ b/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/settings/TimerSettingsScreen.kt
@@ -10,7 +10,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.ArrowBack
+import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CheckboxDefaults
import androidx.compose.material3.Divider
@@ -23,8 +23,6 @@ import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@@ -32,48 +30,45 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
+import org.timemates.app.feature.common.MVI
import org.timemates.app.feature.common.failures.getDefaultDisplayMessage
-import org.timemates.app.foundation.mvi.StateMachine
+import org.timemates.app.feature.common.getFailuresIfPresent
+import org.timemates.app.feature.common.isInvalid
import org.timemates.app.localization.compose.LocalStrings
import org.timemates.app.style.system.appbar.AppBar
import org.timemates.app.style.system.button.ButtonWithProgress
import org.timemates.app.style.system.text_field.SizedOutlinedTextField
import org.timemates.app.style.system.theme.AppTheme
-import org.timemates.app.timers.ui.settings.mvi.TimerSettingsStateMachine.Effect
-import org.timemates.app.timers.ui.settings.mvi.TimerSettingsStateMachine.Event
-import org.timemates.app.timers.ui.settings.mvi.TimerSettingsStateMachine.State
-import io.timemates.sdk.common.constructor.createOrThrow
-import io.timemates.sdk.common.types.value.Count
-import io.timemates.sdk.timers.types.value.TimerDescription
-import io.timemates.sdk.timers.types.value.TimerName
-import kotlinx.coroutines.channels.consumeEach
+import org.timemates.app.timers.ui.settings.mvi.TimerSettingsScreenComponent.*
+import org.timemates.sdk.common.constructor.createOrThrow
+import org.timemates.sdk.common.types.value.Count
+import org.timemates.sdk.timers.types.value.TimerDescription
+import org.timemates.sdk.timers.types.value.TimerName
+import pro.respawn.flowmvi.essenty.compose.subscribe
import kotlin.time.Duration.Companion.minutes
import kotlin.time.DurationUnit
@Composable
fun TimerSettingsScreen(
- stateMachine: StateMachine,
+ mvi: MVI,
navigateToTimersScreen: () -> Unit,
) {
- val state by stateMachine.state.collectAsState()
val snackbarData = remember { SnackbarHostState() }
-
- val nameSize = remember(state.name) { state.name.length }
- val descriptionSize = remember(state.description) { state.description.length }
-
val strings = LocalStrings.current
- LaunchedEffect(Unit) {
- stateMachine.effects.consumeEach { effect ->
- when (effect) {
- is Effect.Failure ->
- snackbarData.showSnackbar(message = effect.throwable.getDefaultDisplayMessage(strings))
+ val state by mvi.subscribe { action ->
+ when (action) {
+ is Action.ShowFailure ->
+ snackbarData.showSnackbar(message = action.throwable.getDefaultDisplayMessage(strings))
- Effect.NavigateToTimersScreen, Effect.Success -> navigateToTimersScreen()
- }
+ Action.NavigateToTimersScreen -> navigateToTimersScreen()
}
}
+ val nameSize = remember(state.name) { state.name.value.length }
+ val descriptionSize = remember(state.description) { state.description.value.length }
+
+
Scaffold(
topBar = {
AppBar(
@@ -82,21 +77,14 @@ fun TimerSettingsScreen(
IconButton(
onClick = { navigateToTimersScreen() },
) {
- Icon(Icons.Rounded.ArrowBack, contentDescription = null)
+ Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = null)
}
},
)
}
) { rootPaddings ->
-
- val nameSupportText = when {
- state.isNameSizeInvalid -> LocalStrings.current.timerNameSizeIsInvalid
- else -> null
- }
- val descriptionYouSupportText = when {
- state.isDescriptionSizeInvalid -> LocalStrings.current.timerDescriptionSizeIsInvalid
- else -> null
- }
+ val nameSupportText = state.name.getFailuresIfPresent(strings)
+ val descriptionYouSupportText = state.description.getFailuresIfPresent(strings)
Column(
modifier = Modifier.fillMaxSize()
@@ -106,34 +94,34 @@ fun TimerSettingsScreen(
) {
SizedOutlinedTextField(
modifier = Modifier.fillMaxWidth(),
- value = state.name,
- onValueChange = { stateMachine.dispatchEvent(Event.NameIsChanged(it)) },
+ value = state.name.value,
+ onValueChange = { mvi.store.intent(Intent.NameIsChanged(it)) },
label = { Text(LocalStrings.current.name) },
- isError = state.isNameSizeInvalid || nameSize > TimerName.SIZE_RANGE.last,
+ isError = state.name.isInvalid() || nameSize > TimerName.LENGTH_RANGE.last,
singleLine = true,
supportingText = {
if (nameSupportText != null) {
Text(nameSupportText)
}
},
- size = IntRange(nameSize, TimerName.SIZE_RANGE.last),
+ size = IntRange(nameSize, TimerName.LENGTH_RANGE.last),
enabled = !state.isLoading,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send),
)
SizedOutlinedTextField(
modifier = Modifier.fillMaxWidth(),
- value = state.description,
- onValueChange = { stateMachine.dispatchEvent(Event.DescriptionIsChanged(it)) },
+ value = state.description.value,
+ onValueChange = { mvi.store.intent(Intent.DescriptionIsChanged(it)) },
label = { Text(LocalStrings.current.description) },
- isError = state.isDescriptionSizeInvalid || descriptionSize > TimerDescription.SIZE_RANGE.last,
+ isError = state.description.isInvalid() || descriptionSize > TimerDescription.LENGTH_RANGE.last,
maxLines = 5,
supportingText = {
if (descriptionYouSupportText != null) {
Text(descriptionYouSupportText)
}
},
- size = IntRange(descriptionSize, TimerDescription.SIZE_RANGE.last),
+ size = IntRange(descriptionSize, TimerDescription.LENGTH_RANGE.last),
enabled = !state.isLoading,
)
@@ -154,7 +142,7 @@ fun TimerSettingsScreen(
modifier = Modifier
.weight(1f),
value = state.workTime.toInt(unit = DurationUnit.MINUTES).toString(),
- onValueChange = { stateMachine.dispatchEvent(Event.WorkTimeIsChanged(it.toInt().minutes)) },
+ onValueChange = { mvi.store.intent(Intent.WorkTimeIsChanged(it.toInt().minutes)) },
label = { Text(LocalStrings.current.workTime) },
singleLine = true,
enabled = !state.isLoading,
@@ -167,7 +155,7 @@ fun TimerSettingsScreen(
modifier = Modifier
.weight(1f),
value = state.restTime.toInt(unit = DurationUnit.MINUTES).toString(),
- onValueChange = { stateMachine.dispatchEvent(Event.RestTimeIsChanged(it.toInt().minutes)) },
+ onValueChange = { mvi.store.intent(Intent.RestTimeIsChanged(it.toInt().minutes)) },
label = { Text(LocalStrings.current.restTime) },
singleLine = true,
enabled = !state.isLoading,
@@ -182,7 +170,7 @@ fun TimerSettingsScreen(
) {
Checkbox(
checked = state.bigRestEnabled,
- onCheckedChange = { stateMachine.dispatchEvent(Event.BigRestModeIsChanged(!state.bigRestEnabled)) },
+ onCheckedChange = { mvi.store.intent(Intent.BigRestModeIsChanged(!state.bigRestEnabled)) },
modifier = Modifier.align(Alignment.CenterVertically),
colors = CheckboxDefaults.colors(checkedColor = AppTheme.colors.primary),
)
@@ -201,8 +189,8 @@ fun TimerSettingsScreen(
) {
OutlinedTextField(
modifier = Modifier.weight(1f),
- value = state.bigRestPer.int.toString(),
- onValueChange = { stateMachine.dispatchEvent(Event.BigRestPerIsChanged(Count.createOrThrow(it.toInt()))) },
+ value = state.bigRestPer.value.toString(),
+ onValueChange = { mvi.store.intent(Intent.BigRestPerIsChanged(Count.factory.createOrThrow(it.toInt()))) },
label = { Text(LocalStrings.current.every) },
singleLine = true,
enabled = !state.isLoading,
@@ -211,7 +199,7 @@ fun TimerSettingsScreen(
OutlinedTextField(
modifier = Modifier.weight(1f),
value = state.bigRestTime.toInt(unit = DurationUnit.MINUTES).toString(),
- onValueChange = { stateMachine.dispatchEvent(Event.BigRestTimeIsChanged(it.toInt().minutes)) },
+ onValueChange = { mvi.store.intent(Intent.BigRestTimeIsChanged(it.toInt().minutes)) },
label = { Text(LocalStrings.current.minutes) },
singleLine = true,
enabled = !state.isLoading,
@@ -234,7 +222,7 @@ fun TimerSettingsScreen(
) {
Checkbox(
checked = state.isEveryoneCanPause,
- onCheckedChange = { stateMachine.dispatchEvent(Event.TimerPauseControlAccessIsChanged(!state.isEveryoneCanPause)) },
+ onCheckedChange = { mvi.store.intent(Intent.TimerPauseControlAccessIsChanged(!state.isEveryoneCanPause)) },
modifier = Modifier.align(Alignment.CenterVertically),
colors = CheckboxDefaults.colors(checkedColor = AppTheme.colors.primary),
)
@@ -251,7 +239,7 @@ fun TimerSettingsScreen(
) {
Checkbox(
checked = state.isConfirmationRequired,
- onCheckedChange = { stateMachine.dispatchEvent(Event.ConfirmationRequirementChanged(!state.isConfirmationRequired)) },
+ onCheckedChange = { mvi.store.intent(Intent.ConfirmationRequirementChanged(!state.isConfirmationRequired)) },
modifier = Modifier.align(Alignment.CenterVertically),
colors = CheckboxDefaults.colors(checkedColor = AppTheme.colors.primary),
)
@@ -278,7 +266,7 @@ fun TimerSettingsScreen(
ButtonWithProgress(
primary = true,
modifier = Modifier.fillMaxWidth(),
- onClick = { stateMachine.dispatchEvent(Event.OnDoneClicked) },
+ onClick = { mvi.store.intent(Intent.OnDoneClicked) },
enabled = !state.isLoading,
isLoading = state.isLoading
) {
diff --git a/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/settings/mvi/TimerSettingsMiddleware.kt b/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/settings/mvi/TimerSettingsMiddleware.kt
deleted file mode 100644
index 991ab73..0000000
--- a/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/settings/mvi/TimerSettingsMiddleware.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package org.timemates.app.timers.ui.settings.mvi
-
-import org.timemates.app.foundation.mvi.Middleware
-import org.timemates.app.foundation.mvi.StateStore
-import org.timemates.app.timers.ui.settings.mvi.TimerSettingsStateMachine.Effect
-import org.timemates.app.timers.ui.settings.mvi.TimerSettingsStateMachine.State
-
-class TimerSettingsMiddleware : Middleware {
- override fun onEffect(effect: Effect, state: State): State {
- return when (effect) {
- is Effect.Failure ->
- state.copy(isLoading = false)
-
- else -> state
- }
- }
-}
\ No newline at end of file
diff --git a/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/settings/mvi/TimerSettingsReducer.kt b/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/settings/mvi/TimerSettingsReducer.kt
deleted file mode 100644
index 625f28e..0000000
--- a/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/settings/mvi/TimerSettingsReducer.kt
+++ /dev/null
@@ -1,111 +0,0 @@
-package org.timemates.app.timers.ui.settings.mvi
-
-import org.timemates.app.foundation.mvi.Reducer
-import org.timemates.app.foundation.mvi.ReducerScope
-import org.timemates.app.timers.ui.settings.mvi.TimerSettingsStateMachine.Effect
-import org.timemates.app.timers.ui.settings.mvi.TimerSettingsStateMachine.Event
-import org.timemates.app.timers.ui.settings.mvi.TimerSettingsStateMachine.State
-import org.timemates.app.users.usecases.TimerSettingsUseCase
-import org.timemates.app.users.validation.TimerDescriptionValidator
-import org.timemates.app.users.validation.TimerNameValidator
-import io.timemates.sdk.common.constructor.createOrThrow
-import io.timemates.sdk.timers.types.TimerSettings
-import io.timemates.sdk.timers.types.value.TimerDescription
-import io.timemates.sdk.timers.types.value.TimerId
-import io.timemates.sdk.timers.types.value.TimerName
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
-
-
-class TimerSettingsReducer(
- private val timerId: TimerId,
- private val timerSettingsUseCase: TimerSettingsUseCase,
- private val timerNameValidator: TimerNameValidator,
- private val timerDescriptionValidator: TimerDescriptionValidator,
-) : Reducer {
- override fun ReducerScope.reduce(state: State, event: Event): State {
- return when (event) {
- Event.OnDoneClicked -> {
- val name = when (timerNameValidator.validate(state.name)) {
- is TimerNameValidator.Result.SizeViolation ->
- return state.copy(isNameSizeInvalid = true)
-
- else -> TimerName.createOrThrow(state.name)
- }
-
- val description = when (timerDescriptionValidator.validate(state.description)) {
- is TimerDescriptionValidator.Result.SizeViolation ->
- return state.copy(isDescriptionSizeInvalid = true)
-
- else -> TimerDescription.createOrThrow(state.description)
- }
-
- editTimer(
- timerId = timerId,
- newName = name,
- newDescription = description,
- settings = TimerSettings.Patch(
- workTime = state.workTime,
- restTime = state.restTime,
- bigRestEnabled = state.bigRestEnabled,
- bigRestPer = state.bigRestPer,
- bigRestTime = state.bigRestTime,
- isEveryoneCanPause = state.isEveryoneCanPause,
- isConfirmationRequired = state.isConfirmationRequired,
- ),
- sendEffect = sendEffect,
- coroutineScope = machineScope,
- )
-
- return state.copy(isLoading = true)
- }
-
-
- is Event.NameIsChanged ->
- state.copy(name = event.name, isNameSizeInvalid = false)
-
- is Event.DescriptionIsChanged ->
- state.copy(description = event.description, isDescriptionSizeInvalid = false)
-
- is Event.WorkTimeIsChanged ->
- state.copy(workTime = event.workTime)
-
- is Event.RestTimeIsChanged ->
- state.copy(workTime = event.restTime)
-
- is Event.BigRestModeIsChanged ->
- state.copy(bigRestEnabled = event.bigRestEnabled)
-
- is Event.BigRestPerIsChanged ->
- state.copy(bigRestPer = event.bigRestPer)
-
- is Event.BigRestTimeIsChanged ->
- state.copy(bigRestTime = event.bigRestTime)
-
- is Event.TimerPauseControlAccessIsChanged ->
- state.copy(isEveryoneCanPause = event.isEveryoneCanPause)
-
- is Event.ConfirmationRequirementChanged ->
- state.copy(isConfirmationRequired = event.isConfirmationRequired)
- }
- }
-
- private fun editTimer(
- timerId: TimerId,
- newName: TimerName,
- newDescription: TimerDescription,
- settings: TimerSettings.Patch?,
- sendEffect: (Effect) -> Unit,
- coroutineScope: CoroutineScope,
- ) {
- coroutineScope.launch {
- when (val result = timerSettingsUseCase.execute(timerId, newName, newDescription, settings)) {
- is TimerSettingsUseCase.Result.Failure ->
- sendEffect(Effect.Failure(result.exception))
-
- is TimerSettingsUseCase.Result.Success ->
- sendEffect(Effect.Success)
- }
- }
- }
-}
diff --git a/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/settings/mvi/TimerSettingsScreenComponent.kt b/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/settings/mvi/TimerSettingsScreenComponent.kt
new file mode 100644
index 0000000..4d30c35
--- /dev/null
+++ b/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/settings/mvi/TimerSettingsScreenComponent.kt
@@ -0,0 +1,155 @@
+package org.timemates.app.timers.ui.settings.mvi
+
+import androidx.compose.runtime.Immutable
+import com.arkivanov.decompose.ComponentContext
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+import org.timemates.app.feature.common.Input
+import org.timemates.app.feature.common.MVI
+import org.timemates.app.feature.common.input
+import org.timemates.app.feature.common.isValid
+import org.timemates.app.timers.ui.settings.mvi.TimerSettingsScreenComponent.Action
+import org.timemates.app.timers.ui.settings.mvi.TimerSettingsScreenComponent.Intent
+import org.timemates.app.timers.ui.settings.mvi.TimerSettingsScreenComponent.State
+import org.timemates.app.users.usecases.TimerSettingsUseCase
+import org.timemates.sdk.common.constructor.createOrThrow
+import org.timemates.sdk.common.types.value.Count
+import org.timemates.sdk.timers.types.TimerSettings
+import org.timemates.sdk.timers.types.value.TimerDescription
+import org.timemates.sdk.timers.types.value.TimerId
+import org.timemates.sdk.timers.types.value.TimerName
+import pro.respawn.flowmvi.api.Container
+import pro.respawn.flowmvi.api.MVIAction
+import pro.respawn.flowmvi.api.MVIIntent
+import pro.respawn.flowmvi.api.MVIState
+import pro.respawn.flowmvi.api.PipelineContext
+import pro.respawn.flowmvi.api.Store
+import pro.respawn.flowmvi.essenty.dsl.retainedStore
+import pro.respawn.flowmvi.plugins.recover
+import pro.respawn.flowmvi.plugins.reduce
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.minutes
+
+class TimerSettingsScreenComponent(
+ componentContext: ComponentContext,
+ private val timerId: TimerId,
+ private val timerSettingsUseCase: TimerSettingsUseCase,
+) : ComponentContext by componentContext, MVI {
+ override val store: Store = retainedStore(initial = State()) {
+ recover { exception ->
+ action(Action.ShowFailure(exception))
+ null
+ }
+
+ reduce { intent ->
+ updateState {
+ when (intent) {
+ is Intent.NameIsChanged -> copy(name = input(intent.name))
+ is Intent.DescriptionIsChanged ->
+ copy(description = input(intent.description))
+
+ is Intent.BigRestModeIsChanged ->
+ copy(bigRestEnabled = intent.bigRestEnabled)
+
+ is Intent.BigRestPerIsChanged ->
+ copy(bigRestPer = bigRestPer)
+
+ is Intent.BigRestTimeIsChanged ->
+ copy(bigRestTime = intent.bigRestTime)
+
+
+ is Intent.RestTimeIsChanged -> copy(restTime = intent.restTime)
+ is Intent.WorkTimeIsChanged -> copy(workTime = intent.workTime)
+
+ is Intent.TimerPauseControlAccessIsChanged ->
+ copy(isEveryoneCanPause = intent.isEveryoneCanPause)
+
+ is Intent.ConfirmationRequirementChanged ->
+ copy(isConfirmationRequired = intent.isConfirmationRequired)
+
+ Intent.OnDoneClicked -> copy(
+ name = name.validated(TimerName.factory),
+ description = description.validated(TimerDescription.factory),
+ ).run {
+ if (name.isValid() && description.isValid() && bigRestPer.isValid()) {
+ saveChangesAsync(
+ name = TimerName.factory.createOrThrow(name.value),
+ description = TimerDescription.factory.createOrThrow(description.value),
+ settings = TimerSettings.Patch(
+ workTime = workTime,
+ restTime = restTime,
+ bigRestTime = bigRestTime,
+ bigRestEnabled = bigRestEnabled,
+ bigRestPer = Count.factory.createOrThrow(bigRestPer.value),
+ isEveryoneCanPause = isEveryoneCanPause,
+ isConfirmationRequired = isConfirmationRequired,
+ ),
+ )
+ copy(isLoading = true)
+ } else this
+ }
+ }
+ }
+ }
+ }
+
+ private suspend fun PipelineContext.saveChangesAsync(
+ name: TimerName,
+ description: TimerDescription,
+ settings: TimerSettings.Patch,
+ ): Unit = coroutineScope {
+ launch {
+ when (val result = timerSettingsUseCase.execute(timerId, name, description, settings)) {
+ is TimerSettingsUseCase.Result.Failure ->
+ action(Action.ShowFailure(result.exception))
+
+ TimerSettingsUseCase.Result.Success -> action(Action.NavigateToTimersScreen)
+ }
+
+ updateState { copy(isLoading = false) }
+ }
+ }
+
+
+ @Immutable
+ data class State(
+ val name: Input = input(""),
+ val description: Input = input(""),
+ val workTime: Duration = 25.minutes,
+ val restTime: Duration = 5.minutes,
+ val bigRestEnabled: Boolean = true,
+ val bigRestPer: Input = input(4),
+ val bigRestTime: Duration = 10.minutes,
+ val isEveryoneCanPause: Boolean = false,
+ val isConfirmationRequired: Boolean = true,
+ val isLoading: Boolean = false,
+ ) : MVIState
+
+ sealed class Intent : MVIIntent {
+ data class NameIsChanged(val name: String) : Intent()
+
+ data class DescriptionIsChanged(val description: String) : Intent()
+
+ data class WorkTimeIsChanged(val workTime: Duration) : Intent()
+
+ data class RestTimeIsChanged(val restTime: Duration) : Intent()
+
+ data class BigRestModeIsChanged(val bigRestEnabled: Boolean) : Intent()
+
+ data class BigRestPerIsChanged(val bigRestPer: Count) : Intent()
+
+ data class BigRestTimeIsChanged(val bigRestTime: Duration) : Intent()
+
+ data class TimerPauseControlAccessIsChanged(val isEveryoneCanPause: Boolean) : Intent()
+
+ data class ConfirmationRequirementChanged(val isConfirmationRequired: Boolean) : Intent()
+
+ data object OnDoneClicked : Intent()
+ }
+
+ sealed class Action : MVIAction {
+ data class ShowFailure(val throwable: Throwable) : Action()
+
+ data object NavigateToTimersScreen : Action()
+ }
+}
diff --git a/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/settings/mvi/TimerSettingsStateMachine.kt b/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/settings/mvi/TimerSettingsStateMachine.kt
deleted file mode 100644
index de37748..0000000
--- a/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/settings/mvi/TimerSettingsStateMachine.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-package org.timemates.app.timers.ui.settings.mvi
-
-import androidx.compose.runtime.Immutable
-import org.timemates.app.foundation.mvi.StateMachine
-import org.timemates.app.foundation.mvi.UiEffect
-import org.timemates.app.foundation.mvi.UiEvent
-import org.timemates.app.foundation.mvi.UiState
-import org.timemates.app.timers.ui.settings.mvi.TimerSettingsStateMachine.Effect
-import org.timemates.app.timers.ui.settings.mvi.TimerSettingsStateMachine.Event
-import org.timemates.app.timers.ui.settings.mvi.TimerSettingsStateMachine.State
-import io.timemates.sdk.common.constructor.createOrThrow
-import io.timemates.sdk.common.types.value.Count
-import kotlin.time.Duration
-import kotlin.time.Duration.Companion.minutes
-
-class TimerSettingsStateMachine(
- reducer: TimerSettingsReducer,
- middleware: TimerSettingsMiddleware,
-) : StateMachine(
- initState = State(),
- reducer = reducer,
- middlewares = listOf(middleware),
-) {
- @Immutable
- data class State(
- val name: String = "",
- val description: String = "",
- val workTime: Duration = 25.minutes,
- val restTime: Duration = 5.minutes,
- val bigRestEnabled: Boolean = true,
- val bigRestPer: Count = Count.createOrThrow(4),
- val bigRestTime: Duration = 10.minutes,
- val isEveryoneCanPause: Boolean = false,
- val isConfirmationRequired: Boolean = true,
- val isNameSizeInvalid: Boolean = false,
- val isDescriptionSizeInvalid: Boolean = false,
- val isLoading: Boolean = false,
- ) : UiState
-
- sealed class Event : UiEvent {
- data class NameIsChanged(val name: String) : Event()
-
- data class DescriptionIsChanged(val description: String) : Event()
-
- data class WorkTimeIsChanged(val workTime: Duration) : Event()
-
- data class RestTimeIsChanged(val restTime: Duration) : Event()
-
- data class BigRestModeIsChanged(val bigRestEnabled: Boolean) : Event()
-
- data class BigRestPerIsChanged(val bigRestPer: Count) : Event()
-
- data class BigRestTimeIsChanged(val bigRestTime: Duration) : Event()
-
- data class TimerPauseControlAccessIsChanged(val isEveryoneCanPause: Boolean) : Event()
-
- data class ConfirmationRequirementChanged(val isConfirmationRequired: Boolean) : Event()
-
- data object OnDoneClicked : Event()
- }
-
- sealed class Effect : UiEffect {
- data class Failure(val throwable: Throwable) : Effect()
-
- data object Success : Effect()
-
- data object NavigateToTimersScreen : Effect()
- }
-}
diff --git a/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/timer_creation/TimerCreationScreen.kt b/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/timer_creation/TimerCreationScreen.kt
index c3bc23a..fb3dafa 100644
--- a/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/timer_creation/TimerCreationScreen.kt
+++ b/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/timer_creation/TimerCreationScreen.kt
@@ -13,10 +13,10 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.rounded.ArrowBack
+import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CheckboxDefaults
-import androidx.compose.material3.Divider
+import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
@@ -26,8 +26,6 @@ import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@@ -35,52 +33,46 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
+import org.timemates.app.feature.common.MVI
import org.timemates.app.feature.common.failures.getDefaultDisplayMessage
-import org.timemates.app.foundation.mvi.StateMachine
+import org.timemates.app.feature.common.getFailuresIfPresent
+import org.timemates.app.feature.common.isInvalid
import org.timemates.app.localization.compose.LocalStrings
import org.timemates.app.style.system.appbar.AppBar
import org.timemates.app.style.system.button.ButtonWithProgress
import org.timemates.app.style.system.text_field.SizedOutlinedTextField
import org.timemates.app.style.system.theme.AppTheme
-import org.timemates.app.timers.ui.timer_creation.mvi.TimerCreationStateMachine.Effect
-import org.timemates.app.timers.ui.timer_creation.mvi.TimerCreationStateMachine.Event
-import org.timemates.app.timers.ui.timer_creation.mvi.TimerCreationStateMachine.State
-import io.timemates.sdk.common.constructor.createOrThrow
-import io.timemates.sdk.common.types.value.Count
-import io.timemates.sdk.timers.types.value.TimerDescription
-import io.timemates.sdk.timers.types.value.TimerName
-import kotlinx.coroutines.channels.consumeEach
+import org.timemates.app.timers.ui.timer_creation.mvi.TimerCreationScreenComponent.*
+import org.timemates.sdk.timers.types.value.TimerDescription
+import org.timemates.sdk.timers.types.value.TimerName
+import pro.respawn.flowmvi.essenty.compose.subscribe
import kotlin.time.Duration.Companion.minutes
import kotlin.time.DurationUnit
@Composable
fun TimerCreationScreen(
- stateMachine: StateMachine,
+ mvi: MVI,
navigateToTimersScreen: () -> Unit,
) {
- val state by stateMachine.state.collectAsState()
- val snackbarData = remember { SnackbarHostState() }
-
- val nameSize = remember(state.name) { state.name.length }
- val descriptionSize = remember(state.description) { state.description.length }
-
val strings = LocalStrings.current
+ val snackBarData = remember { SnackbarHostState() }
- LaunchedEffect(Unit) {
- stateMachine.effects.consumeEach { effect ->
- when (effect) {
- is Effect.Failure ->
- snackbarData.showSnackbar(
- message = effect.throwable.getDefaultDisplayMessage(strings)
- )
+ val state by mvi.subscribe { action ->
+ when (action) {
+ is Action.ShowFailure ->
+ snackBarData.showSnackbar(
+ message = action.throwable.getDefaultDisplayMessage(strings)
+ )
- Effect.NavigateToTimersScreen ->
- navigateToTimersScreen()
- }
+ Action.NavigateToTimersScreen ->
+ navigateToTimersScreen()
}
}
+ val nameSize = remember(state.name) { state.name.value.length }
+ val descriptionSize = remember(state.description) { state.description.value.length }
+
Scaffold(
topBar = {
AppBar(
@@ -89,21 +81,14 @@ fun TimerCreationScreen(
IconButton(
onClick = { navigateToTimersScreen() },
) {
- Icon(Icons.Rounded.ArrowBack, contentDescription = null)
+ Icon(Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = null)
}
},
)
}
) { rootPaddings ->
-
- val nameSupportText = when {
- state.isNameSizeInvalid -> LocalStrings.current.timerNameSizeIsInvalid
- else -> null
- }
- val descriptionYouSupportText = when {
- state.isDescriptionSizeInvalid -> LocalStrings.current.timerDescriptionSizeIsInvalid
- else -> null
- }
+ val nameSupportText = state.name.getFailuresIfPresent(strings)
+ val descriptionYouSupportText = state.description.getFailuresIfPresent(strings)
Column(
modifier = Modifier.fillMaxSize()
@@ -116,38 +101,38 @@ fun TimerCreationScreen(
SizedOutlinedTextField(
modifier = Modifier.fillMaxWidth(),
- value = state.name,
- onValueChange = { stateMachine.dispatchEvent(Event.NameIsChanged(it)) },
+ value = state.name.value,
+ onValueChange = { mvi.store.intent(Intent.NameIsChanged(it)) },
label = { Text(LocalStrings.current.name) },
- isError = state.isNameSizeInvalid || nameSize > TimerName.SIZE_RANGE.last,
+ isError = state.name.isInvalid() || nameSize > TimerName.LENGTH_RANGE.last,
singleLine = true,
supportingText = {
if (nameSupportText != null) {
Text(nameSupportText)
}
},
- size = IntRange(nameSize, TimerName.SIZE_RANGE.last),
+ size = IntRange(nameSize, TimerName.LENGTH_RANGE.last),
enabled = !state.isLoading,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send),
)
SizedOutlinedTextField(
modifier = Modifier.fillMaxWidth(),
- value = state.description,
- onValueChange = { stateMachine.dispatchEvent(Event.DescriptionIsChanged(it)) },
+ value = state.description.value,
+ onValueChange = { mvi.store.intent(Intent.DescriptionIsChanged(it)) },
label = { Text(LocalStrings.current.description) },
- isError = state.isDescriptionSizeInvalid || descriptionSize > TimerDescription.SIZE_RANGE.last,
+ isError = state.description.isInvalid() || descriptionSize > TimerDescription.LENGTH_RANGE.last,
maxLines = 5,
supportingText = {
if (descriptionYouSupportText != null) {
Text(descriptionYouSupportText)
}
},
- size = IntRange(descriptionSize, TimerDescription.SIZE_RANGE.last),
+ size = IntRange(descriptionSize, TimerDescription.LENGTH_RANGE.last),
enabled = !state.isLoading,
)
- Divider(
+ HorizontalDivider(
color = AppTheme.colors.secondary,
thickness = 1.dp,
modifier = Modifier
@@ -165,8 +150,8 @@ fun TimerCreationScreen(
.weight(1f),
value = state.workTime.toInt(unit = DurationUnit.MINUTES).toString(),
onValueChange = {
- stateMachine.dispatchEvent(
- Event.WorkTimeIsChanged(it.toIntOrNull()?.minutes ?: state.workTime)
+ mvi.store.intent(
+ Intent.WorkTimeIsChanged(it.toIntOrNull()?.minutes ?: state.workTime)
)
},
label = { Text(LocalStrings.current.workTime) },
@@ -181,7 +166,7 @@ fun TimerCreationScreen(
modifier = Modifier
.weight(1f),
value = state.restTime.toInt(unit = DurationUnit.MINUTES).toString(),
- onValueChange = { stateMachine.dispatchEvent(Event.RestTimeIsChanged(it.toInt().minutes)) },
+ onValueChange = { mvi.store.intent(Intent.RestTimeIsChanged(it.toInt().minutes)) },
label = { Text(LocalStrings.current.restTime) },
singleLine = true,
enabled = !state.isLoading,
@@ -196,15 +181,16 @@ fun TimerCreationScreen(
) {
Checkbox(
checked = state.bigRestEnabled,
- onCheckedChange = { stateMachine.dispatchEvent(Event.BigRestModeIsChanged(!state.bigRestEnabled)) },
+ onCheckedChange = { mvi.store.intent(Intent.BigRestModeIsChanged(!state.bigRestEnabled)) },
modifier = Modifier.align(Alignment.CenterVertically),
colors = CheckboxDefaults.colors(checkedColor = AppTheme.colors.primary),
+ enabled = !state.isLoading,
)
Text(
text = LocalStrings.current.advancedRestSettingsDescription,
modifier = Modifier.align(Alignment.CenterVertically),
- color = AppTheme.colors.primary,
+ color = if (!state.isLoading) AppTheme.colors.primary else AppTheme.colors.secondary,
)
}
@@ -215,8 +201,8 @@ fun TimerCreationScreen(
) {
OutlinedTextField(
modifier = Modifier.weight(1f),
- value = state.bigRestPer.int.toString(),
- onValueChange = { stateMachine.dispatchEvent(Event.BigRestPerIsChanged(Count.createOrThrow(it.toInt()))) },
+ value = state.bigRestPer.value.toString(),
+ onValueChange = { mvi.store.intent(Intent.BigRestPerIsChanged(it.toInt())) },
label = { Text(LocalStrings.current.every) },
singleLine = true,
enabled = !state.isLoading,
@@ -225,7 +211,7 @@ fun TimerCreationScreen(
OutlinedTextField(
modifier = Modifier.weight(1f),
value = state.bigRestTime.toInt(unit = DurationUnit.MINUTES).toString(),
- onValueChange = { stateMachine.dispatchEvent(Event.BigRestTimeIsChanged(it.toInt().minutes)) },
+ onValueChange = { mvi.store.intent(Intent.BigRestTimeIsChanged(it.toInt().minutes)) },
label = { Text(LocalStrings.current.minutes) },
singleLine = true,
enabled = !state.isLoading,
@@ -235,7 +221,7 @@ fun TimerCreationScreen(
Spacer(modifier = Modifier.padding(8.dp))
- Divider(
+ HorizontalDivider(
color = AppTheme.colors.secondary,
thickness = 1.dp,
modifier = Modifier
@@ -248,15 +234,16 @@ fun TimerCreationScreen(
) {
Checkbox(
checked = state.isEveryoneCanPause,
- onCheckedChange = { stateMachine.dispatchEvent(Event.TimerPauseControlAccessIsChanged(!state.isEveryoneCanPause)) },
+ onCheckedChange = { mvi.store.intent(Intent.TimerPauseControlAccessIsChanged(!state.isEveryoneCanPause)) },
modifier = Modifier.align(Alignment.CenterVertically),
colors = CheckboxDefaults.colors(checkedColor = AppTheme.colors.primary),
+ enabled = !state.isLoading,
)
Text(
text = LocalStrings.current.publicManageTimerStateDescription,
modifier = Modifier.align(Alignment.CenterVertically),
- color = AppTheme.colors.primary,
+ color = if (!state.isLoading) AppTheme.colors.primary else AppTheme.colors.secondary,
)
}
@@ -265,15 +252,16 @@ fun TimerCreationScreen(
) {
Checkbox(
checked = state.isConfirmationRequired,
- onCheckedChange = { stateMachine.dispatchEvent(Event.ConfirmationRequirementChanged(!state.isConfirmationRequired)) },
+ onCheckedChange = { mvi.store.intent(Intent.ConfirmationRequirementChanged(!state.isConfirmationRequired)) },
modifier = Modifier.align(Alignment.CenterVertically),
colors = CheckboxDefaults.colors(checkedColor = AppTheme.colors.primary),
+ enabled = !state.isLoading,
)
Text(
text = LocalStrings.current.confirmationRequiredDescription,
modifier = Modifier.align(Alignment.CenterVertically),
- color = AppTheme.colors.primary,
+ color = if (!state.isLoading) AppTheme.colors.primary else AppTheme.colors.secondary,
)
}
@@ -284,7 +272,7 @@ fun TimerCreationScreen(
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
SnackbarHost(
- hostState = snackbarData
+ hostState = snackBarData
) {
Snackbar(it)
}
@@ -292,8 +280,8 @@ fun TimerCreationScreen(
ButtonWithProgress(
primary = true,
modifier = Modifier.fillMaxWidth(),
- onClick = { stateMachine.dispatchEvent(Event.OnDoneClicked) },
- enabled = !state.isLoading,
+ onClick = { mvi.store.intent(Intent.OnDoneClicked) },
+ enabled = !state.isLoading && state.canAddMoreTimers,
isLoading = state.isLoading
) {
Text(LocalStrings.current.save)
diff --git a/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/timer_creation/mvi/TimerCreationMiddleware.kt b/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/timer_creation/mvi/TimerCreationMiddleware.kt
deleted file mode 100644
index fba8b97..0000000
--- a/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/timer_creation/mvi/TimerCreationMiddleware.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package org.timemates.app.timers.ui.timer_creation.mvi
-
-import org.timemates.app.foundation.mvi.Middleware
-import org.timemates.app.foundation.mvi.StateStore
-import org.timemates.app.timers.ui.timer_creation.mvi.TimerCreationStateMachine.Effect
-import org.timemates.app.timers.ui.timer_creation.mvi.TimerCreationStateMachine.State
-
-class TimerCreationMiddleware : Middleware {
- override fun onEffect(effect: Effect, state: State): State {
- return when (effect) {
- is Effect.Failure ->
- state.copy(isLoading = false)
-
- else -> state
- }
- }
-}
\ No newline at end of file
diff --git a/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/timer_creation/mvi/TimerCreationReducer.kt b/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/timer_creation/mvi/TimerCreationReducer.kt
deleted file mode 100644
index aaa5a08..0000000
--- a/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/timer_creation/mvi/TimerCreationReducer.kt
+++ /dev/null
@@ -1,106 +0,0 @@
-package org.timemates.app.timers.ui.timer_creation.mvi
-
-import org.timemates.app.foundation.mvi.Reducer
-import org.timemates.app.foundation.mvi.ReducerScope
-import org.timemates.app.timers.ui.timer_creation.mvi.TimerCreationStateMachine.Effect
-import org.timemates.app.timers.ui.timer_creation.mvi.TimerCreationStateMachine.Event
-import org.timemates.app.timers.ui.timer_creation.mvi.TimerCreationStateMachine.State
-import org.timemates.app.users.usecases.TimerCreationUseCase
-import org.timemates.app.users.validation.TimerDescriptionValidator
-import org.timemates.app.users.validation.TimerNameValidator
-import io.timemates.sdk.common.constructor.createOrThrow
-import io.timemates.sdk.timers.types.TimerSettings
-import io.timemates.sdk.timers.types.value.TimerDescription
-import io.timemates.sdk.timers.types.value.TimerName
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.launch
-
-class TimerCreationReducer(
- private val timerCreationUseCase: TimerCreationUseCase,
- private val timerNameValidator: TimerNameValidator,
- private val timerDescriptionValidator: TimerDescriptionValidator,
-) : Reducer {
- override fun ReducerScope.reduce(state: State, event: Event): State {
- return when (event) {
- Event.OnDoneClicked -> {
- val name = when (timerNameValidator.validate(state.name)) {
- is TimerNameValidator.Result.SizeViolation ->
- return state.copy(isNameSizeInvalid = true)
-
- else -> TimerName.createOrThrow(state.name)
- }
-
- val description = when (timerDescriptionValidator.validate(state.description)) {
- is TimerDescriptionValidator.Result.SizeViolation ->
- return state.copy(isDescriptionSizeInvalid = true)
-
- else -> TimerDescription.createOrThrow(state.description)
- }
-
- createTimer(
- name = name,
- description = description,
- settings = TimerSettings(
- state.workTime,
- state.restTime,
- state.bigRestTime,
- state.bigRestEnabled,
- state.bigRestPer,
- state.isEveryoneCanPause,
- state.isConfirmationRequired,
- ),
- sendEffect = sendEffect,
- coroutineScope = machineScope,
- )
-
- return state.copy(isLoading = true)
- }
-
- is Event.NameIsChanged ->
- state.copy(name = event.name, isNameSizeInvalid = false)
-
- is Event.DescriptionIsChanged ->
- state.copy(description = event.description, isDescriptionSizeInvalid = false)
-
- is Event.WorkTimeIsChanged ->
- state.copy(workTime = event.workTime)
-
- is Event.RestTimeIsChanged ->
- state.copy(workTime = event.restTime)
-
- is Event.BigRestModeIsChanged ->
- state.copy(bigRestEnabled = event.bigRestEnabled)
-
- is Event.BigRestPerIsChanged ->
- state.copy(bigRestPer = event.bigRestPer)
-
- is Event.BigRestTimeIsChanged ->
- state.copy(bigRestTime = event.bigRestTime)
-
- is Event.TimerPauseControlAccessIsChanged ->
- state.copy(isEveryoneCanPause = event.isEveryoneCanPause)
-
- is Event.ConfirmationRequirementChanged ->
- state.copy(isConfirmationRequired = event.isConfirmationRequired)
- }
- }
-
- private fun createTimer(
- name: TimerName,
- description: TimerDescription,
- settings: TimerSettings,
- sendEffect: (Effect) -> Unit,
- coroutineScope: CoroutineScope,
- ) {
- coroutineScope.launch {
- when (val result = timerCreationUseCase.execute(name, description, settings)) {
- is TimerCreationUseCase.Result.Failure ->
- sendEffect(Effect.Failure(result.exception))
-
- is TimerCreationUseCase.Result.Success -> {
- sendEffect(Effect.NavigateToTimersScreen)
- }
- }
- }
- }
-}
diff --git a/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/timer_creation/mvi/TimerCreationScreenComponent.kt b/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/timer_creation/mvi/TimerCreationScreenComponent.kt
new file mode 100644
index 0000000..13e6baf
--- /dev/null
+++ b/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/timer_creation/mvi/TimerCreationScreenComponent.kt
@@ -0,0 +1,157 @@
+package org.timemates.app.timers.ui.timer_creation.mvi
+
+import androidx.compose.runtime.Immutable
+import com.arkivanov.decompose.ComponentContext
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+import org.timemates.app.feature.common.Input
+import org.timemates.app.feature.common.MVI
+import org.timemates.app.feature.common.input
+import org.timemates.app.feature.common.isValid
+import org.timemates.app.timers.ui.timer_creation.mvi.TimerCreationScreenComponent.Action
+import org.timemates.app.timers.ui.timer_creation.mvi.TimerCreationScreenComponent.Intent
+import org.timemates.app.timers.ui.timer_creation.mvi.TimerCreationScreenComponent.State
+import org.timemates.app.users.usecases.TimerCreationUseCase
+import org.timemates.sdk.common.constructor.createOrThrow
+import org.timemates.sdk.common.types.value.Count
+import org.timemates.sdk.timers.types.TimerSettings
+import org.timemates.sdk.timers.types.value.TimerDescription
+import org.timemates.sdk.timers.types.value.TimerName
+import pro.respawn.flowmvi.api.Container
+import pro.respawn.flowmvi.api.MVIAction
+import pro.respawn.flowmvi.api.MVIIntent
+import pro.respawn.flowmvi.api.MVIState
+import pro.respawn.flowmvi.api.PipelineContext
+import pro.respawn.flowmvi.api.Store
+import pro.respawn.flowmvi.essenty.dsl.retainedStore
+import pro.respawn.flowmvi.plugins.recover
+import pro.respawn.flowmvi.plugins.reduce
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.minutes
+
+class TimerCreationScreenComponent(
+ componentContext: ComponentContext,
+ private val timerCreationUseCase: TimerCreationUseCase,
+) : ComponentContext by componentContext, MVI {
+
+ override val store: Store = retainedStore(initial = State()) {
+ recover { exception ->
+ action(Action.ShowFailure(exception))
+ null
+ }
+
+ reduce { intent ->
+ updateState {
+ when (intent) {
+ is Intent.NameIsChanged -> copy(name = input(intent.name))
+ is Intent.DescriptionIsChanged ->
+ copy(description = input(intent.description))
+
+ is Intent.BigRestModeIsChanged ->
+ copy(bigRestEnabled = intent.bigRestEnabled)
+
+ is Intent.BigRestPerIsChanged ->
+ copy(bigRestPer = bigRestPer)
+
+ is Intent.BigRestTimeIsChanged ->
+ copy(bigRestTime = intent.bigRestTime)
+
+
+ is Intent.RestTimeIsChanged -> copy(restTime = intent.restTime)
+ is Intent.WorkTimeIsChanged -> copy(workTime = intent.workTime)
+
+ is Intent.TimerPauseControlAccessIsChanged ->
+ copy(isEveryoneCanPause = intent.isEveryoneCanPause)
+
+ is Intent.ConfirmationRequirementChanged ->
+ copy(isConfirmationRequired = intent.isConfirmationRequired)
+
+ Intent.OnDoneClicked -> copy(
+ name = name.validated(TimerName.factory),
+ description = description.validated(TimerDescription.factory),
+ bigRestPer = bigRestPer.validated(Count.factory),
+ ).run {
+ if (name.isValid() && description.isValid() && bigRestPer.isValid()) {
+ createTimerAsync(
+ name = TimerName.factory.createOrThrow(name.value),
+ description = TimerDescription.factory.createOrThrow(description.value),
+ settings = TimerSettings(
+ workTime = workTime,
+ restTime = restTime,
+ bigRestTime = bigRestTime,
+ bigRestEnabled = bigRestEnabled,
+ bigRestPer = Count.factory.createOrThrow(bigRestPer.value),
+ isEveryoneCanPause = isEveryoneCanPause,
+ isConfirmationRequired = isConfirmationRequired,
+ ),
+ )
+ copy(isLoading = true)
+ } else this
+ }
+ }
+ }
+ }
+ }
+
+ private fun PipelineContext.createTimerAsync(
+ name: TimerName,
+ description: TimerDescription,
+ settings: TimerSettings,
+ ) {
+ launch {
+ when (val result = timerCreationUseCase.execute(name, description, settings)) {
+ is TimerCreationUseCase.Result.Failure -> {
+ action(Action.ShowFailure(result.exception))
+ updateState { copy(isLoading = false, canAddMoreTimers = false) }
+ }
+
+ is TimerCreationUseCase.Result.Success -> {
+ action(Action.NavigateToTimersScreen)
+ }
+ }
+ }
+ }
+
+ @Immutable
+ data class State(
+ val name: Input = input(""),
+ val description: Input = input(""),
+ val workTime: Duration = 25.minutes,
+ val restTime: Duration = 5.minutes,
+ val bigRestEnabled: Boolean = true,
+ val bigRestPer: Input = input(4),
+ val bigRestTime: Duration = 10.minutes,
+ val isEveryoneCanPause: Boolean = false,
+ val isConfirmationRequired: Boolean = true,
+ val isLoading: Boolean = false,
+ val canAddMoreTimers: Boolean = true,
+ ) : MVIState
+
+ sealed class Intent : MVIIntent {
+ data class NameIsChanged(val name: String) : Intent()
+
+ data class DescriptionIsChanged(val description: String) : Intent()
+
+ data class WorkTimeIsChanged(val workTime: Duration) : Intent()
+
+ data class RestTimeIsChanged(val restTime: Duration) : Intent()
+
+ data class BigRestModeIsChanged(val bigRestEnabled: Boolean) : Intent()
+
+ data class BigRestPerIsChanged(val bigRestPer: Int) : Intent()
+
+ data class BigRestTimeIsChanged(val bigRestTime: Duration) : Intent()
+
+ data class TimerPauseControlAccessIsChanged(val isEveryoneCanPause: Boolean) : Intent()
+
+ data class ConfirmationRequirementChanged(val isConfirmationRequired: Boolean) : Intent()
+
+ data object OnDoneClicked : Intent()
+ }
+
+ sealed class Action : MVIAction {
+ data class ShowFailure(val throwable: Throwable) : Action()
+
+ data object NavigateToTimersScreen : Action()
+ }
+}
diff --git a/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/timer_creation/mvi/TimerCreationStateMachine.kt b/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/timer_creation/mvi/TimerCreationStateMachine.kt
deleted file mode 100644
index 7ea4b7a..0000000
--- a/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/timer_creation/mvi/TimerCreationStateMachine.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-package org.timemates.app.timers.ui.timer_creation.mvi
-
-import androidx.compose.runtime.Immutable
-import org.timemates.app.foundation.mvi.StateMachine
-import org.timemates.app.foundation.mvi.UiEffect
-import org.timemates.app.foundation.mvi.UiEvent
-import org.timemates.app.foundation.mvi.UiState
-import org.timemates.app.timers.ui.timer_creation.mvi.TimerCreationStateMachine.Effect
-import org.timemates.app.timers.ui.timer_creation.mvi.TimerCreationStateMachine.Event
-import org.timemates.app.timers.ui.timer_creation.mvi.TimerCreationStateMachine.State
-import io.timemates.sdk.common.constructor.createOrThrow
-import io.timemates.sdk.common.types.value.Count
-import kotlin.time.Duration
-import kotlin.time.Duration.Companion.minutes
-
-class TimerCreationStateMachine(
- reducer: TimerCreationReducer,
- middleware: TimerCreationMiddleware,
-) : StateMachine(
- initState = State(),
- reducer = reducer,
- middlewares = listOf(middleware),
-) {
-
- @Immutable
- data class State(
- val name: String = "",
- val description: String = "",
- val workTime: Duration = 25.minutes,
- val restTime: Duration = 5.minutes,
- val bigRestEnabled: Boolean = true,
- val bigRestPer: Count = Count.createOrThrow(4),
- val bigRestTime: Duration = 10.minutes,
- val isEveryoneCanPause: Boolean = false,
- val isConfirmationRequired: Boolean = true,
- val isNameSizeInvalid: Boolean = false,
- val isDescriptionSizeInvalid: Boolean = false,
- val isLoading: Boolean = false,
- ) : UiState
-
- sealed class Event : UiEvent {
- data class NameIsChanged(val name: String) : Event()
-
- data class DescriptionIsChanged(val description: String) : Event()
-
- data class WorkTimeIsChanged(val workTime: Duration) : Event()
-
- data class RestTimeIsChanged(val restTime: Duration) : Event()
-
- data class BigRestModeIsChanged(val bigRestEnabled: Boolean) : Event()
-
- data class BigRestPerIsChanged(val bigRestPer: Count) : Event()
-
- data class BigRestTimeIsChanged(val bigRestTime: Duration) : Event()
-
- data class TimerPauseControlAccessIsChanged(val isEveryoneCanPause: Boolean) : Event()
-
- data class ConfirmationRequirementChanged(val isConfirmationRequired: Boolean) : Event()
-
- object OnDoneClicked : Event()
- }
-
- sealed class Effect : UiEffect {
- data class Failure(val throwable: Throwable) : Effect()
-
- data object NavigateToTimersScreen : Effect()
- }
-}
diff --git a/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/timers_list/TimersListScreen.kt b/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/timers_list/TimersListScreen.kt
index 5a82ea0..f4b9b84 100644
--- a/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/timers_list/TimersListScreen.kt
+++ b/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/timers_list/TimersListScreen.kt
@@ -22,63 +22,45 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
-import dev.icerock.moko.resources.compose.painterResource
-import io.github.skeptick.libres.compose.painterResource
+import org.timemates.app.feature.common.MVI
import org.timemates.app.feature.common.failures.getDefaultDisplayMessage
-import org.timemates.app.foundation.mvi.StateMachine
import org.timemates.app.localization.compose.LocalStrings
-import org.timemates.app.style.system.Resources
import org.timemates.app.style.system.appbar.AppBar
+import org.timemates.app.style.system.button.FloatingActionButton
+import org.timemates.app.style.system.theme.AppTheme
import org.timemates.app.timers.ui.PlaceholderTimerItem
import org.timemates.app.timers.ui.TimerItem
-import org.timemates.app.style.system.theme.AppTheme
-import org.timemates.app.timers.ui.timers_list.mvi.TimersListStateMachine.Effect
-import org.timemates.app.timers.ui.timers_list.mvi.TimersListStateMachine.Event
-import org.timemates.app.timers.ui.timers_list.mvi.TimersListStateMachine.State
-import org.timemates.app.style.system.button.FloatingActionButton
-import io.timemates.sdk.timers.types.value.TimerId
-import kotlinx.coroutines.channels.consumeEach
+import org.timemates.app.timers.ui.timers_list.mvi.TimersListScreenComponent.*
+import pro.respawn.flowmvi.essenty.compose.subscribe
@Composable
fun TimersListScreen(
- stateMachine: StateMachine,
+ mvi: MVI,
navigateToSetting: () -> Unit,
navigateToTimerCreationScreen: () -> Unit,
- navigateToTimer: (TimerId) -> Unit,
+ navigateToTimer: (Long) -> Unit,
) {
- val state by stateMachine.state.collectAsState()
val snackbarData = remember { SnackbarHostState() }
- val timersListState = rememberLazyListState()
-
- val painter: Painter = Resources.image.empty_list_image.painterResource()
-
val strings = LocalStrings.current
- LaunchedEffect(true) {
- stateMachine.dispatchEvent(Event.Load)
-
- stateMachine.effects.consumeEach { effect ->
- when (effect) {
- is Effect.Failure ->
- snackbarData.showSnackbar(message = effect.throwable.getDefaultDisplayMessage(strings))
-
- else -> {}
- }
+ val state by mvi.subscribe { action ->
+ when (action) {
+ is Action.Failure ->
+ snackbarData.showSnackbar(message = action.throwable.getDefaultDisplayMessage(strings))
}
}
+ val timersListState = rememberLazyListState()
- if(state.hasMoreItems) {
+ if (state.hasMoreItems) {
LaunchedEffect(timersListState.layoutInfo.visibleItemsInfo.lastOrNull()) {
if (timersListState.isScrolledToTheEnd()) {
- stateMachine.dispatchEvent(Event.Load)
+ mvi.store.intent(Intent.Load)
}
}
}
@@ -147,7 +129,7 @@ fun TimersListScreen(
items(state.timersList) { timer ->
TimerItem(
timer = timer,
- onClick = { navigateToTimer(timer.timerId) }
+ onClick = { navigateToTimer(timer.timerId.long) }
)
}
@@ -166,7 +148,7 @@ fun TimersListScreen(
}
}
-fun LazyListState.isScrolledToTheEnd() : Boolean {
+fun LazyListState.isScrolledToTheEnd(): Boolean {
val lastItem = layoutInfo.visibleItemsInfo.lastOrNull()
return lastItem == null || lastItem.size + lastItem.offset <= layoutInfo.viewportEndOffset
}
diff --git a/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/timers_list/mvi/TimersListMiddleware.kt b/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/timers_list/mvi/TimersListMiddleware.kt
deleted file mode 100644
index 5a91837..0000000
--- a/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/timers_list/mvi/TimersListMiddleware.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package org.timemates.app.timers.ui.timers_list.mvi
-
-import org.timemates.app.foundation.mvi.Middleware
-import org.timemates.app.foundation.mvi.StateStore
-import org.timemates.app.timers.ui.timers_list.mvi.TimersListStateMachine.State
-import org.timemates.app.timers.ui.timers_list.mvi.TimersListStateMachine.Effect
-
-class TimersListMiddleware : Middleware {
- override fun onEffect(effect: Effect, state: State): State {
- return when(effect) {
- is Effect.Failure -> {
- state.copy(isLoading = false, isError = true)
- }
-
- is Effect.NoMoreTimers -> {
- state.copy(hasMoreItems = false, isLoading = false)
- }
-
- is Effect.LoadTimers -> {
- state.copy(timersList = (state.timersList + effect.timersList).distinct())
- }
- }
- }
-}
\ No newline at end of file
diff --git a/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/timers_list/mvi/TimersListReducer.kt b/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/timers_list/mvi/TimersListReducer.kt
deleted file mode 100644
index 021161a..0000000
--- a/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/timers_list/mvi/TimersListReducer.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-package org.timemates.app.timers.ui.timers_list.mvi
-
-import org.timemates.app.foundation.mvi.Reducer
-import org.timemates.app.foundation.mvi.ReducerScope
-import org.timemates.app.timers.ui.timers_list.mvi.TimersListStateMachine.Effect
-import org.timemates.app.timers.ui.timers_list.mvi.TimersListStateMachine.Event
-import org.timemates.app.timers.ui.timers_list.mvi.TimersListStateMachine.State
-import org.timemates.app.users.usecases.GetUserTimersUseCase
-import io.timemates.sdk.common.pagination.PagesIterator
-import io.timemates.sdk.timers.types.Timer
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.CoroutineStart
-import kotlinx.coroutines.async
-import kotlinx.coroutines.launch
-
-class TimersListReducer (
- private val getUserTimersUseCase: GetUserTimersUseCase,
-) : Reducer {
- private var pagesIterator: PagesIterator? = null
-
- override fun ReducerScope.reduce(state: State, event: Event): State {
- when(event) {
- is Event.Load -> {
- getUserTimers(sendEffect, machineScope)
- return state.copy(isLoading = pagesIterator == null || pagesIterator?.hasNext() == true)
- }
- }
- }
-
- private fun getUserTimers(
- sendEffect: (Effect) -> Unit,
- scope: CoroutineScope,
- ) {
- scope.launch {
- if(pagesIterator == null) {
- when (val result = getUserTimersUseCase.execute()) {
- is GetUserTimersUseCase.Result.Success -> {
- this@TimersListReducer.pagesIterator = result.list
- }
- }
- }
-
- val pagesIterator = pagesIterator!!
-
- if (pagesIterator.hasNext()) {
- val currentResult = pagesIterator.next()
- currentResult
- .onSuccess {
- sendEffect(Effect.LoadTimers(currentResult.getOrThrow()))
- }.onFailure {
- sendEffect(Effect.Failure(currentResult.exceptionOrNull()!!))
- }
- } else {
- sendEffect(Effect.NoMoreTimers)
- }
- }
- }
-}
diff --git a/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/timers_list/mvi/TimersListScreenComponent.kt b/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/timers_list/mvi/TimersListScreenComponent.kt
new file mode 100644
index 0000000..167a731
--- /dev/null
+++ b/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/timers_list/mvi/TimersListScreenComponent.kt
@@ -0,0 +1,117 @@
+package org.timemates.app.timers.ui.timers_list.mvi
+
+import androidx.compose.runtime.Immutable
+import com.arkivanov.decompose.ComponentContext
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.toImmutableList
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import org.timemates.app.feature.common.MVI
+import org.timemates.app.timers.ui.timers_list.mvi.TimersListScreenComponent.Action
+import org.timemates.app.timers.ui.timers_list.mvi.TimersListScreenComponent.Intent
+import org.timemates.app.timers.ui.timers_list.mvi.TimersListScreenComponent.State
+import org.timemates.app.users.repositories.TimersRepository
+import org.timemates.app.users.usecases.GetUserTimersUseCase
+import org.timemates.sdk.common.pagination.PagesIterator
+import org.timemates.sdk.timers.types.Timer
+import pro.respawn.flowmvi.api.Container
+import pro.respawn.flowmvi.api.MVIAction
+import pro.respawn.flowmvi.api.MVIIntent
+import pro.respawn.flowmvi.api.MVIState
+import pro.respawn.flowmvi.api.PipelineContext
+import pro.respawn.flowmvi.api.Store
+import pro.respawn.flowmvi.essenty.dsl.retainedStore
+import pro.respawn.flowmvi.plugins.cache
+import pro.respawn.flowmvi.plugins.init
+import pro.respawn.flowmvi.plugins.recover
+import pro.respawn.flowmvi.plugins.reduce
+
+class TimersListScreenComponent(
+ componentContext: ComponentContext,
+ private val getUserTimersUseCase: GetUserTimersUseCase,
+ private val timersRepository: TimersRepository,
+) : ComponentContext by componentContext, MVI {
+
+ override val store: Store = retainedStore(initial = State(isLoading = true)) {
+ recover { exception ->
+ action(Action.Failure(exception))
+ null
+ }
+
+ val pagesIterator by cache {
+ when (val result = getUserTimersUseCase.execute()) {
+ is GetUserTimersUseCase.Result.Success ->
+ result.list
+ }
+ }
+
+ init {
+ loadTimers(pagesIterator)
+ }
+
+ reduce { intent ->
+ when (intent) {
+ Intent.Load -> {
+ updateState { copy(isLoading = true) }
+ loadTimers(pagesIterator)
+ checkUpdates()
+ }
+ }
+ }
+ }
+
+ private fun PipelineContext.loadTimers(pagesIterator: PagesIterator) {
+ launch {
+ val hasNext = pagesIterator.hasNext()
+
+ if (hasNext) {
+ val currentResult = pagesIterator.next()
+ currentResult
+ .onSuccess {
+ updateState { copy(timersList = (timersList + currentResult.getOrThrow()).toImmutableList()) }
+ }.onFailure {
+ action(Action.Failure(currentResult.exceptionOrNull()!!))
+ }
+ }
+
+ updateState { copy(isLoading = false, hasMoreItems = hasMoreItems) }
+ }
+ }
+
+ private suspend fun PipelineContext.checkUpdates() {
+ timersRepository.getTimersUpdates().collect { update ->
+ updateState {
+ when (update) {
+ is TimersRepository.TimerUpdateAction.Added ->
+ copy(timersList = (listOf(update.timer) + timersList).toImmutableList())
+
+ is TimersRepository.TimerUpdateAction.Deleted ->
+ copy(timersList = timersList.filter { it.timerId != update.timerId }.toImmutableList())
+
+ is TimersRepository.TimerUpdateAction.Updated ->
+ copy(timersList = timersList.map {
+ if (it.timerId == update.timer.timerId) update.timer else it
+ }.toImmutableList())
+ }
+ }
+
+ }
+ }
+
+ @Immutable
+ data class State(
+ val timersList: ImmutableList = persistentListOf(),
+ val hasMoreItems: Boolean = true,
+ val isLoading: Boolean = false,
+ val isError: Boolean = false,
+ ) : MVIState
+
+ sealed class Intent : MVIIntent {
+ data object Load : Intent()
+ }
+
+ sealed class Action : MVIAction {
+ data class Failure(val throwable: Throwable) : Action()
+ }
+}
\ No newline at end of file
diff --git a/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/timers_list/mvi/TimersListStateMachine.kt b/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/timers_list/mvi/TimersListStateMachine.kt
deleted file mode 100644
index f357ddc..0000000
--- a/feature/timers/presentation/src/commonMain/kotlin/org/timemates/app/timers/ui/timers_list/mvi/TimersListStateMachine.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-package org.timemates.app.timers.ui.timers_list.mvi
-
-import androidx.compose.runtime.Immutable
-import org.timemates.app.foundation.mvi.StateMachine
-import org.timemates.app.foundation.mvi.UiEffect
-import org.timemates.app.foundation.mvi.UiEvent
-import org.timemates.app.foundation.mvi.UiState
-import org.timemates.app.timers.ui.timers_list.mvi.TimersListStateMachine.Effect
-import org.timemates.app.timers.ui.timers_list.mvi.TimersListStateMachine.Event
-import org.timemates.app.timers.ui.timers_list.mvi.TimersListStateMachine.State
-import io.timemates.sdk.timers.types.Timer
-
-class TimersListStateMachine(
- reducer: TimersListReducer,
- middleware: TimersListMiddleware,
-) : StateMachine(
- initState = State(),
- reducer = reducer,
- middlewares = listOf(middleware),
-) {
- @Immutable
- data class State(
- val timersList: List = emptyList(),
- val hasMoreItems: Boolean = true,
- val isLoading: Boolean = false,
- val isError: Boolean = false,
- ): UiState
-
- sealed class Event : UiEvent {
- data object Load : Event()
- }
-
- sealed class Effect : UiEffect {
- data class Failure(val throwable: Throwable) : Effect()
-
- data class LoadTimers(val timersList: List) : Effect()
-
- data object NoMoreTimers : Effect()
- }
-}
diff --git a/feature/timers/presentation/src/jvmTest/kotlin/org/timemates/app/timers/ui/timers_list/TimersListMiddlewareTest.kt b/feature/timers/presentation/src/jvmTest/kotlin/org/timemates/app/timers/ui/timers_list/TimersListMiddlewareTest.kt
deleted file mode 100644
index 4e39640..0000000
--- a/feature/timers/presentation/src/jvmTest/kotlin/org/timemates/app/timers/ui/timers_list/TimersListMiddlewareTest.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-package org.timemates.app.timers.ui.timers_list
-
-import io.mockk.every
-import io.mockk.mockk
-import org.timemates.app.foundation.mvi.StateStore
-import org.timemates.app.timers.ui.timers_list.mvi.TimersListMiddleware
-import org.timemates.app.timers.ui.timers_list.mvi.TimersListStateMachine
-import kotlinx.coroutines.flow.MutableStateFlow
-import org.junit.jupiter.api.Test
-
-class TimersListMiddlewareTest {
- private val stateStore: StateStore = mockk()
- private val middleware: TimersListMiddleware = TimersListMiddleware()
-
- @Test
- fun `effects produced by network operations should remove loading status`() {
- // GIVEN
- val effects = listOf(
- TimersListStateMachine.Effect.Failure(Exception()),
- TimersListStateMachine.Effect.NoMoreTimers
- )
- every { stateStore.state } returns MutableStateFlow(TimersListStateMachine.State(isLoading = true))
-
- // WHEN
- effects.map { effect -> effect to middleware.onEffect(effect, stateStore) }
- // THEN
- .forEach { (effect, state) ->
- assert(!state.isLoading) {
- "${effect::class.simpleName} effect does not change loading status to false."
- }
- }
- }
-}
diff --git a/feature/timers/presentation/src/jvmTest/kotlin/org/timemates/app/timers/ui/settings/TimerSettingsMiddlewareTest.kt b/feature/timers/presentation/src/jvmTestOld/kotlin/org/timemates/app/timers/ui/settings/TimerSettingsMiddlewareTest.kt
similarity index 51%
rename from feature/timers/presentation/src/jvmTest/kotlin/org/timemates/app/timers/ui/settings/TimerSettingsMiddlewareTest.kt
rename to feature/timers/presentation/src/jvmTestOld/kotlin/org/timemates/app/timers/ui/settings/TimerSettingsMiddlewareTest.kt
index 2b84a32..6138ce1 100644
--- a/feature/timers/presentation/src/jvmTest/kotlin/org/timemates/app/timers/ui/settings/TimerSettingsMiddlewareTest.kt
+++ b/feature/timers/presentation/src/jvmTestOld/kotlin/org/timemates/app/timers/ui/settings/TimerSettingsMiddlewareTest.kt
@@ -1,25 +1,18 @@
package org.timemates.app.timers.ui.settings
-import io.mockk.every
-import io.mockk.mockk
-import org.timemates.app.foundation.mvi.StateStore
-import org.timemates.app.timers.ui.settings.mvi.TimerSettingsMiddleware
-import org.timemates.app.timers.ui.settings.mvi.TimerSettingsStateMachine
-import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.jupiter.api.Test
+import org.timemates.app.timers.ui.settings.mvi.TimerSettingsScreenComponent
class TimerSettingsMiddlewareTest {
- private val stateStore: StateStore = mockk()
private val middleware: TimerSettingsMiddleware = TimerSettingsMiddleware()
@Test
fun `effects produced by network operations should remove loading status`() {
//GIVEN
- val effects = listOf(TimerSettingsStateMachine.Effect.Failure(Exception()))
- every { stateStore.state } returns MutableStateFlow(TimerSettingsStateMachine.State(isLoading = true))
+ val effects = listOf(TimerSettingsScreenComponent.Effect.Failure(Exception()))
//WHEN
- effects.map { effect -> effect to middleware.onEffect(effect, stateStore) }
+ effects.map { effect -> effect to middleware.onEffect(effect, TimerSettingsScreenComponent.State(isLoading = true)) }
//THEN
.forEach { (effect, state) ->
assert(!state.isLoading) {
diff --git a/feature/timers/presentation/src/jvmTest/kotlin/org/timemates/app/timers/ui/settings/TimerSettingsReducerTest.kt b/feature/timers/presentation/src/jvmTestOld/kotlin/org/timemates/app/timers/ui/settings/TimerSettingsReducerTest.kt
similarity index 88%
rename from feature/timers/presentation/src/jvmTest/kotlin/org/timemates/app/timers/ui/settings/TimerSettingsReducerTest.kt
rename to feature/timers/presentation/src/jvmTestOld/kotlin/org/timemates/app/timers/ui/settings/TimerSettingsReducerTest.kt
index 4cbb42d..5b35b03 100644
--- a/feature/timers/presentation/src/jvmTest/kotlin/org/timemates/app/timers/ui/settings/TimerSettingsReducerTest.kt
+++ b/feature/timers/presentation/src/jvmTestOld/kotlin/org/timemates/app/timers/ui/settings/TimerSettingsReducerTest.kt
@@ -2,19 +2,15 @@ package org.timemates.app.timers.ui.settings
import io.mockk.every
import io.mockk.mockk
-import org.timemates.app.foundation.mvi.reduce
-import org.timemates.app.timers.ui.settings.mvi.TimerSettingsReducer
-import org.timemates.app.timers.ui.settings.mvi.TimerSettingsStateMachine.Event
-import org.timemates.app.timers.ui.settings.mvi.TimerSettingsStateMachine.State
-import org.timemates.app.users.usecases.TimerSettingsUseCase
-import org.timemates.app.users.validation.TimerDescriptionValidator
-import org.timemates.app.users.validation.TimerNameValidator
-import io.timemates.sdk.common.constructor.createOrThrow
-import io.timemates.sdk.common.types.value.Count
-import io.timemates.sdk.timers.types.value.TimerId
-import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.TestScope
import org.junit.jupiter.api.Test
+import org.timemates.app.foundation.mvi.reduce
+import org.timemates.app.timers.ui.settings.mvi.TimerSettingsScreenComponent.Intent
+import org.timemates.app.timers.ui.settings.mvi.TimerSettingsScreenComponent.State
+import org.timemates.app.users.usecases.TimerSettingsUseCase
+import org.timemates.sdk.common.constructor.createOrThrow
+import org.timemates.sdk.common.types.value.Count
+import org.timemates.sdk.timers.types.value.TimerId
import kotlin.test.assertEquals
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes
@@ -32,7 +28,7 @@ class TimerSettingsReducerTest {
private val workTime: Duration = 5.minutes
private val restTime: Duration = 1.minutes
private val bigRestEnabled: Boolean = true
- private val bigRestPer: Count = Count.createOrThrow(2)
+ private val bigRestPer: Count = Count.factory.createOrThrow(2)
private val bigRestTime: Duration = 6.minutes
private val isEveryoneCanPause: Boolean = true
private val isConfirmationRequired: Boolean = false
@@ -56,7 +52,7 @@ class TimerSettingsReducerTest {
// WHEN
val result = reducer.reduce(
state = State(name = validName, description = validDescription),
- event = Event.OnDoneClicked,
+ event = Intent.OnDoneClicked,
machineScope = scope,
) {}
@@ -84,7 +80,7 @@ class TimerSettingsReducerTest {
// WHEN
val result = reducer.reduce(
state = State(name = validName, description = validDescription),
- event = Event.OnDoneClicked,
+ event = Intent.OnDoneClicked,
machineScope = scope
) {}
@@ -112,7 +108,7 @@ class TimerSettingsReducerTest {
// WHEN
val result = reducer.reduce(
state = State(name = invalidName, description = validDescription),
- event = Event.OnDoneClicked,
+ event = Intent.OnDoneClicked,
machineScope = scope,
) {}
@@ -140,7 +136,7 @@ class TimerSettingsReducerTest {
// WHEN
val result = reducer.reduce(
State(name = validName, description = invalidDescription),
- Event.OnDoneClicked,
+ Intent.OnDoneClicked,
scope,
) {}
@@ -178,7 +174,7 @@ class TimerSettingsReducerTest {
isEveryoneCanPause = isEveryoneCanPause,
isConfirmationRequired = isConfirmationRequired
),
- event = Event.OnDoneClicked,
+ event = Intent.OnDoneClicked,
machineScope = scope,
) {}
diff --git a/feature/timers/presentation/src/jvmTest/kotlin/org/timemates/app/timers/ui/timer_creation/TimerCreationMiddlewareTest.kt b/feature/timers/presentation/src/jvmTestOld/kotlin/org/timemates/app/timers/ui/timer_creation/TimerCreationMiddlewareTest.kt
similarity index 61%
rename from feature/timers/presentation/src/jvmTest/kotlin/org/timemates/app/timers/ui/timer_creation/TimerCreationMiddlewareTest.kt
rename to feature/timers/presentation/src/jvmTestOld/kotlin/org/timemates/app/timers/ui/timer_creation/TimerCreationMiddlewareTest.kt
index cfff25d..4d54b0d 100644
--- a/feature/timers/presentation/src/jvmTest/kotlin/org/timemates/app/timers/ui/timer_creation/TimerCreationMiddlewareTest.kt
+++ b/feature/timers/presentation/src/jvmTestOld/kotlin/org/timemates/app/timers/ui/timer_creation/TimerCreationMiddlewareTest.kt
@@ -1,26 +1,19 @@
package org.timemates.app.timers.ui.timer_creation
-import io.mockk.every
-import io.mockk.mockk
-import org.timemates.app.foundation.mvi.StateStore
-import org.timemates.app.timers.ui.timer_creation.mvi.TimerCreationMiddleware
-import org.timemates.app.timers.ui.timer_creation.mvi.TimerCreationStateMachine.Effect
-import org.timemates.app.timers.ui.timer_creation.mvi.TimerCreationStateMachine.State
-import kotlinx.coroutines.flow.MutableStateFlow
import org.junit.jupiter.api.Test
+import org.timemates.app.timers.ui.timer_creation.mvi.TimerCreationScreenComponent.Action
+import org.timemates.app.timers.ui.timer_creation.mvi.TimerCreationScreenComponent.State
class TimerCreationMiddlewareTest {
- private val stateStore: StateStore = mockk()
private val middleware: TimerCreationMiddleware = TimerCreationMiddleware()
@Test
fun `effects produced by network operations should remove loading status`() {
//GIVEN
- val effects = listOf(Effect.Failure(Exception()))
- every { stateStore.state } returns MutableStateFlow(State(isLoading = true))
+ val effects = listOf(Action.ShowFailure(Exception()))
//WHEN
- effects.map { effect -> effect to middleware.onEffect(effect, stateStore) }
+ effects.map { effect -> effect to middleware.onEffect(effect, State(isLoading = true)) }
//THEN
.forEach { (effect, state) ->
assert(!state.isLoading) {
diff --git a/feature/timers/presentation/src/jvmTest/kotlin/org/timemates/app/timers/ui/timer_creation/TimerCreationReducerTest.kt b/feature/timers/presentation/src/jvmTestOld/kotlin/org/timemates/app/timers/ui/timer_creation/TimerCreationReducerTest.kt
similarity index 79%
rename from feature/timers/presentation/src/jvmTest/kotlin/org/timemates/app/timers/ui/timer_creation/TimerCreationReducerTest.kt
rename to feature/timers/presentation/src/jvmTestOld/kotlin/org/timemates/app/timers/ui/timer_creation/TimerCreationReducerTest.kt
index 0e4405b..2971170 100644
--- a/feature/timers/presentation/src/jvmTest/kotlin/org/timemates/app/timers/ui/timer_creation/TimerCreationReducerTest.kt
+++ b/feature/timers/presentation/src/jvmTestOld/kotlin/org/timemates/app/timers/ui/timer_creation/TimerCreationReducerTest.kt
@@ -2,17 +2,13 @@ package org.timemates.app.timers.ui.timer_creation
import io.mockk.every
import io.mockk.mockk
-import org.timemates.app.foundation.mvi.reduce
-import org.timemates.app.timers.ui.timer_creation.mvi.TimerCreationReducer
-import org.timemates.app.timers.ui.timer_creation.mvi.TimerCreationStateMachine
-import org.timemates.app.users.usecases.TimerCreationUseCase
-import org.timemates.app.users.validation.TimerDescriptionValidator
-import org.timemates.app.users.validation.TimerNameValidator
-import io.timemates.sdk.common.constructor.createOrThrow
-import io.timemates.sdk.common.types.value.Count
-import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.TestScope
import org.junit.jupiter.api.Test
+import org.timemates.app.foundation.mvi.reduce
+import org.timemates.app.timers.ui.timer_creation.mvi.TimerCreationScreenComponent
+import org.timemates.app.users.usecases.TimerCreationUseCase
+import org.timemates.sdk.common.constructor.createOrThrow
+import org.timemates.sdk.common.types.value.Count
import kotlin.test.assertEquals
import kotlin.time.Duration
import kotlin.time.Duration.Companion.minutes
@@ -29,7 +25,7 @@ class TimerCreationReducerTest {
private val workTime: Duration = 5.minutes
private val restTime: Duration = 1.minutes
private val bigRestEnabled: Boolean = true
- private val bigRestPer: Count = Count.createOrThrow(2)
+ private val bigRestPer: Count = Count.factory.createOrThrow(2)
private val bigRestTime: Duration = 6.minutes
private val isEveryoneCanPause: Boolean = true
private val isConfirmationRequired: Boolean = false
@@ -51,14 +47,14 @@ class TimerCreationReducerTest {
// WHEN
val result = reducer.reduce(
- state = TimerCreationStateMachine.State(name = validName, description = validDescription),
- event = TimerCreationStateMachine.Event.OnDoneClicked,
+ state = TimerCreationScreenComponent.State(name = validName, description = validDescription),
+ event = TimerCreationScreenComponent.Intent.OnDoneClicked,
machineScope = scope,
) {}
// THEN
assertEquals(
- expected = TimerCreationStateMachine.State(
+ expected = TimerCreationScreenComponent.State(
name = validName,
description = validDescription,
isNameSizeInvalid = false,
@@ -79,14 +75,14 @@ class TimerCreationReducerTest {
// WHEN
val result = reducer.reduce(
- TimerCreationStateMachine.State(name = validName, description = validDescription),
- TimerCreationStateMachine.Event.OnDoneClicked,
+ TimerCreationScreenComponent.State(name = validName, description = validDescription),
+ TimerCreationScreenComponent.Intent.OnDoneClicked,
scope,
) {}
// THEN
assertEquals(
- expected = TimerCreationStateMachine.State(
+ expected = TimerCreationScreenComponent.State(
name = validName,
description = validDescription,
isNameSizeInvalid = false,
@@ -107,14 +103,14 @@ class TimerCreationReducerTest {
// WHEN
val result = reducer.reduce(
- state = TimerCreationStateMachine.State(name = invalidName, description = validDescription),
- event = TimerCreationStateMachine.Event.OnDoneClicked,
+ state = TimerCreationScreenComponent.State(name = invalidName, description = validDescription),
+ event = TimerCreationScreenComponent.Intent.OnDoneClicked,
machineScope = scope,
) {}
// THEN
assertEquals(
- expected = TimerCreationStateMachine.State(
+ expected = TimerCreationScreenComponent.State(
name = invalidName,
description = validDescription,
isNameSizeInvalid = true,
@@ -135,14 +131,14 @@ class TimerCreationReducerTest {
// WHEN
val result = reducer.reduce(
- state = TimerCreationStateMachine.State(name = validName, description = invalidDescription),
- event = TimerCreationStateMachine.Event.OnDoneClicked,
+ state = TimerCreationScreenComponent.State(name = validName, description = invalidDescription),
+ event = TimerCreationScreenComponent.Intent.OnDoneClicked,
machineScope = scope,
) {}
// THEN
assertEquals(
- expected = TimerCreationStateMachine.State(
+ expected = TimerCreationScreenComponent.State(
name = validName,
description = invalidDescription,
isNameSizeInvalid = false,
@@ -163,7 +159,7 @@ class TimerCreationReducerTest {
// WHEN
val result = reducer.reduce(
- state = TimerCreationStateMachine.State(
+ state = TimerCreationScreenComponent.State(
name = validName,
description = validDescription,
workTime = workTime,
@@ -174,13 +170,13 @@ class TimerCreationReducerTest {
isEveryoneCanPause = isEveryoneCanPause,
isConfirmationRequired = isConfirmationRequired
),
- event = TimerCreationStateMachine.Event.OnDoneClicked,
+ event = TimerCreationScreenComponent.Intent.OnDoneClicked,
machineScope = scope,
) {}
// THEN
assertEquals(
- expected = TimerCreationStateMachine.State(
+ expected = TimerCreationScreenComponent.State(
name = validName,
description = validDescription,
workTime = workTime,
diff --git a/feature/timers/presentation/src/jvmTestOld/kotlin/org/timemates/app/timers/ui/timers_list/TimersListMiddlewareTest.kt b/feature/timers/presentation/src/jvmTestOld/kotlin/org/timemates/app/timers/ui/timers_list/TimersListMiddlewareTest.kt
new file mode 100644
index 0000000..b67c83d
--- /dev/null
+++ b/feature/timers/presentation/src/jvmTestOld/kotlin/org/timemates/app/timers/ui/timers_list/TimersListMiddlewareTest.kt
@@ -0,0 +1,27 @@
+package org.timemates.app.timers.ui.timers_list
+
+import org.junit.jupiter.api.Test
+import org.timemates.app.timers.ui.timers_list.mvi.TimersListScreenComponent
+import org.timemates.app.timers.ui.timers_list.mvi.TimersListScreenComponent.State
+
+class TimersListMiddlewareTest {
+ private val middleware: TimersListMiddleware = TimersListMiddleware()
+
+ @Test
+ fun `effects produced by network operations should remove loading status`() {
+ // GIVEN
+ val actions = listOf(
+ TimersListScreenComponent.Action.Failure(Exception()),
+ TimersListScreenComponent.Action.NoMoreTimers
+ )
+
+ // WHEN
+ actions.map { effect -> effect to middleware.onEffect(effect, State(isLoading = true)) }
+ // THEN
+ .forEach { (effect, state) ->
+ assert(!state.isLoading) {
+ "${effect::class.simpleName} effect does not change loading status to false."
+ }
+ }
+ }
+}
diff --git a/feature/timers/presentation/src/jvmTest/kotlin/org/timemates/app/timers/ui/timers_list/TimersListReducerTest.kt b/feature/timers/presentation/src/jvmTestOld/kotlin/org/timemates/app/timers/ui/timers_list/TimersListReducerTest.kt
similarity index 70%
rename from feature/timers/presentation/src/jvmTest/kotlin/org/timemates/app/timers/ui/timers_list/TimersListReducerTest.kt
rename to feature/timers/presentation/src/jvmTestOld/kotlin/org/timemates/app/timers/ui/timers_list/TimersListReducerTest.kt
index 388bf32..4889d1a 100644
--- a/feature/timers/presentation/src/jvmTest/kotlin/org/timemates/app/timers/ui/timers_list/TimersListReducerTest.kt
+++ b/feature/timers/presentation/src/jvmTestOld/kotlin/org/timemates/app/timers/ui/timers_list/TimersListReducerTest.kt
@@ -1,13 +1,11 @@
package org.timemates.app.timers.ui.timers_list
import io.mockk.mockk
-import org.timemates.app.foundation.mvi.reduce
-import org.timemates.app.timers.ui.timers_list.mvi.TimersListReducer
-import org.timemates.app.timers.ui.timers_list.mvi.TimersListStateMachine
-import org.timemates.app.users.usecases.GetUserTimersUseCase
-import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.TestScope
import org.junit.jupiter.api.Test
+import org.timemates.app.foundation.mvi.reduce
+import org.timemates.app.timers.ui.timers_list.mvi.TimersListScreenComponent
+import org.timemates.app.users.usecases.GetUserTimersUseCase
import kotlin.test.assertEquals
class TimersListReducerTest {
@@ -22,14 +20,14 @@ class TimersListReducerTest {
fun `Event Load must return the parameter isLoading true`() {
// WHEN
val result = reducer.reduce(
- state = TimersListStateMachine.State(),
- event = TimersListStateMachine.Event.Load,
+ state = TimersListScreenComponent.State(),
+ event = TimersListScreenComponent.Intent.Load,
machineScope = scope,
) {}
// THEN
assertEquals(
- expected = TimersListStateMachine.State(
+ expected = TimersListScreenComponent.State(
isLoading = true,
),
actual = result,
diff --git a/feature/users/data/src/commonMain/kotlin/org/timemates/app/users/data/CachedUsersDataSource.kt b/feature/users/data/src/commonMain/kotlin/org/timemates/app/users/data/CachedUsersDataSource.kt
index 98c97ec..d8b8fe5 100644
--- a/feature/users/data/src/commonMain/kotlin/org/timemates/app/users/data/CachedUsersDataSource.kt
+++ b/feature/users/data/src/commonMain/kotlin/org/timemates/app/users/data/CachedUsersDataSource.kt
@@ -1,16 +1,16 @@
package org.timemates.app.users.data
-import org.timemates.app.users.data.database.CachedUsersQueries
-import io.timemates.sdk.common.constructor.createOrNull
-import io.timemates.sdk.common.constructor.createOrThrow
-import io.timemates.sdk.users.profile.types.Avatar
-import io.timemates.sdk.users.profile.types.User
-import io.timemates.sdk.users.profile.types.value.EmailAddress
-import io.timemates.sdk.users.profile.types.value.UserDescription
-import io.timemates.sdk.users.profile.types.value.UserId
-import io.timemates.sdk.users.profile.types.value.UserName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
+import org.timemates.app.users.data.database.CachedUsersQueries
+import org.timemates.sdk.common.constructor.createOrNull
+import org.timemates.sdk.common.constructor.createOrThrow
+import org.timemates.sdk.users.profile.types.Avatar
+import org.timemates.sdk.users.profile.types.User
+import org.timemates.sdk.users.profile.types.value.EmailAddress
+import org.timemates.sdk.users.profile.types.value.UserDescription
+import org.timemates.sdk.users.profile.types.value.UserId
+import org.timemates.sdk.users.profile.types.value.UserName
class CachedUsersDataSource(
private val cachedUsersQueries: CachedUsersQueries,
@@ -42,10 +42,10 @@ class CachedUsersDataSource(
val user = withContext(Dispatchers.IO) { cachedUsersQueries.get(id.long).executeAsOneOrNull() }
?: return null
- val name = UserName.createOrNull(user.name) ?: return null
- val description = UserDescription.createOrNull(user.description) ?: return null
- val avatar = user.gravatarId?.let { Avatar.GravatarId.createOrThrow(it) }
- val emailAddress = user.emailAddress?.let { emailAddress -> EmailAddress.createOrNull(emailAddress) }
+ val name = UserName.factory.createOrNull(user.name) ?: return null
+ val description = UserDescription.factory.createOrNull(user.description) ?: return null
+ val avatar = user.gravatarId?.let { Avatar.GravatarId.factory.createOrThrow(it) }
+ val emailAddress = user.emailAddress?.let { emailAddress -> EmailAddress.factory.createOrNull(emailAddress) }
return User(
id = id,
diff --git a/feature/users/data/src/commonMain/kotlin/org/timemates/app/users/data/UsersRepository.kt b/feature/users/data/src/commonMain/kotlin/org/timemates/app/users/data/UsersRepository.kt
index d382ee2..2a13612 100644
--- a/feature/users/data/src/commonMain/kotlin/org/timemates/app/users/data/UsersRepository.kt
+++ b/feature/users/data/src/commonMain/kotlin/org/timemates/app/users/data/UsersRepository.kt
@@ -1,23 +1,23 @@
package org.timemates.app.users.data
-import org.timemates.app.foundation.time.TimeProvider
-import io.timemates.sdk.common.types.Empty
-import io.timemates.sdk.users.UserApi
-import io.timemates.sdk.users.profile.types.User
-import io.timemates.sdk.users.profile.types.value.UserDescription
-import io.timemates.sdk.users.profile.types.value.UserId
-import io.timemates.sdk.users.profile.types.value.UserName
+import org.timemates.sdk.common.types.Empty
+import org.timemates.sdk.users.UserApi
+import org.timemates.sdk.users.profile.types.User
+import org.timemates.sdk.users.profile.types.value.UserDescription
+import org.timemates.sdk.users.profile.types.value.UserId
+import org.timemates.sdk.users.profile.types.value.UserName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
+import org.timemates.app.foundation.time.TimeProvider
import org.timemates.app.users.repositories.UsersRepository as UserRepositoryContract
class UsersRepository(
private val userApi: UserApi,
private val cachedUsersDataSource: CachedUsersDataSource,
private val timeProvider: TimeProvider,
- private val coroutineScope: CoroutineScope,
+ coroutineScope: CoroutineScope,
) : UserRepositoryContract {
init {
diff --git a/feature/users/dependencies/build.gradle.kts b/feature/users/dependencies/build.gradle.kts
index e8e8602..ed287e2 100644
--- a/feature/users/dependencies/build.gradle.kts
+++ b/feature/users/dependencies/build.gradle.kts
@@ -26,5 +26,5 @@ dependencies {
commonMainImplementation(projects.feature.users.domain)
commonMainImplementation(projects.feature.users.data.database)
commonMainImplementation(projects.feature.users.presentation)
- commonMainImplementation(projects.feature.common.domain)
+ commonMainImplementation(projects.core.ui)
}
\ No newline at end of file
diff --git a/feature/users/dependencies/src/commonMain/kotlin/org/timemates/app/users/dependencies/UsersDataModule.kt b/feature/users/dependencies/src/commonMain/kotlin/org/timemates/app/users/dependencies/UsersDataModule.kt
index 03071fd..a97e56d 100644
--- a/feature/users/dependencies/src/commonMain/kotlin/org/timemates/app/users/dependencies/UsersDataModule.kt
+++ b/feature/users/dependencies/src/commonMain/kotlin/org/timemates/app/users/dependencies/UsersDataModule.kt
@@ -5,9 +5,9 @@ import org.timemates.app.foundation.time.TimeProvider
import org.timemates.app.users.data.CachedUsersDataSource
import org.timemates.app.users.data.UsersRepository
import org.timemates.app.users.data.database.TimeMatesUsers
-import io.timemates.sdk.common.engine.TimeMatesRequestsEngine
-import io.timemates.sdk.common.providers.AccessHashProvider
-import io.timemates.sdk.users.UserApi
+import org.timemates.sdk.common.engine.TimeMatesRequestsEngine
+import org.timemates.sdk.common.providers.AccessHashProvider
+import org.timemates.sdk.users.UserApi
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import org.koin.core.annotation.Factory
diff --git a/feature/users/dependencies/src/commonMain/kotlin/org/timemates/app/users/dependencies/UsersModule.kt b/feature/users/dependencies/src/commonMain/kotlin/org/timemates/app/users/dependencies/UsersModule.kt
new file mode 100644
index 0000000..6dfdc0b
--- /dev/null
+++ b/feature/users/dependencies/src/commonMain/kotlin/org/timemates/app/users/dependencies/UsersModule.kt
@@ -0,0 +1,10 @@
+package org.timemates.app.users.dependencies
+
+import org.koin.core.annotation.Module
+
+@Module(
+ includes = [
+ UsersDataModule::class,
+ ]
+)
+class UsersModule
\ No newline at end of file
diff --git a/feature/users/domain/src/commonMain/kotlin/org/timemates/app/users/repositories/AuthorizationsRepository.kt b/feature/users/domain/src/commonMain/kotlin/org/timemates/app/users/repositories/AuthorizationsRepository.kt
index 6f71144..0b11a3b 100644
--- a/feature/users/domain/src/commonMain/kotlin/org/timemates/app/users/repositories/AuthorizationsRepository.kt
+++ b/feature/users/domain/src/commonMain/kotlin/org/timemates/app/users/repositories/AuthorizationsRepository.kt
@@ -1,6 +1,6 @@
package org.timemates.app.users.repositories
-import io.timemates.sdk.users.profile.types.value.UserId
+import org.timemates.sdk.users.profile.types.value.UserId
interface AuthorizationsRepository {
/**
diff --git a/feature/users/domain/src/commonMain/kotlin/org/timemates/app/users/repositories/UsersRepository.kt b/feature/users/domain/src/commonMain/kotlin/org/timemates/app/users/repositories/UsersRepository.kt
index dfa97df..84490dc 100644
--- a/feature/users/domain/src/commonMain/kotlin/org/timemates/app/users/repositories/UsersRepository.kt
+++ b/feature/users/domain/src/commonMain/kotlin/org/timemates/app/users/repositories/UsersRepository.kt
@@ -1,10 +1,10 @@
package org.timemates.app.users.repositories
-import io.timemates.sdk.common.types.Empty
-import io.timemates.sdk.users.profile.types.User
-import io.timemates.sdk.users.profile.types.value.UserDescription
-import io.timemates.sdk.users.profile.types.value.UserId
-import io.timemates.sdk.users.profile.types.value.UserName
+import org.timemates.sdk.common.types.Empty
+import org.timemates.sdk.users.profile.types.User
+import org.timemates.sdk.users.profile.types.value.UserDescription
+import org.timemates.sdk.users.profile.types.value.UserId
+import org.timemates.sdk.users.profile.types.value.UserName
import kotlinx.coroutines.flow.Flow
/**
diff --git a/feature/users/domain/src/commonMain/kotlin/org/timemates/app/users/usecases/EditUserUseCase.kt b/feature/users/domain/src/commonMain/kotlin/org/timemates/app/users/usecases/EditUserUseCase.kt
index 77079e3..187fb74 100644
--- a/feature/users/domain/src/commonMain/kotlin/org/timemates/app/users/usecases/EditUserUseCase.kt
+++ b/feature/users/domain/src/commonMain/kotlin/org/timemates/app/users/usecases/EditUserUseCase.kt
@@ -2,8 +2,8 @@ package org.timemates.app.users.usecases
import org.timemates.app.users.repositories.AuthorizationsRepository
import org.timemates.app.users.repositories.UsersRepository
-import io.timemates.sdk.users.profile.types.value.UserDescription
-import io.timemates.sdk.users.profile.types.value.UserName
+import org.timemates.sdk.users.profile.types.value.UserDescription
+import org.timemates.sdk.users.profile.types.value.UserName
/**
* Use case class for editing a user's information.
diff --git a/feature/users/domain/src/commonMain/kotlin/org/timemates/app/users/usecases/GetUserUseCase.kt b/feature/users/domain/src/commonMain/kotlin/org/timemates/app/users/usecases/GetUserUseCase.kt
index 371cd5f..e39081a 100644
--- a/feature/users/domain/src/commonMain/kotlin/org/timemates/app/users/usecases/GetUserUseCase.kt
+++ b/feature/users/domain/src/commonMain/kotlin/org/timemates/app/users/usecases/GetUserUseCase.kt
@@ -1,9 +1,9 @@
package org.timemates.app.users.usecases
import org.timemates.app.users.repositories.UsersRepository
-import io.timemates.sdk.common.exceptions.NotFoundException
-import io.timemates.sdk.users.profile.types.User
-import io.timemates.sdk.users.profile.types.value.UserId
+import org.timemates.sdk.common.exceptions.NotFoundException
+import org.timemates.sdk.users.profile.types.User
+import org.timemates.sdk.users.profile.types.value.UserId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
diff --git a/feature/users/domain/src/commonMain/kotlin/org/timemates/app/users/usecases/GetUsersUseCase.kt b/feature/users/domain/src/commonMain/kotlin/org/timemates/app/users/usecases/GetUsersUseCase.kt
index c453cbc..bfa65fb 100644
--- a/feature/users/domain/src/commonMain/kotlin/org/timemates/app/users/usecases/GetUsersUseCase.kt
+++ b/feature/users/domain/src/commonMain/kotlin/org/timemates/app/users/usecases/GetUsersUseCase.kt
@@ -1,9 +1,9 @@
package org.timemates.app.users.usecases
import org.timemates.app.users.repositories.UsersRepository
-import io.timemates.sdk.common.exceptions.NotFoundException
-import io.timemates.sdk.users.profile.types.User
-import io.timemates.sdk.users.profile.types.value.UserId
+import org.timemates.sdk.common.exceptions.NotFoundException
+import org.timemates.sdk.users.profile.types.User
+import org.timemates.sdk.users.profile.types.value.UserId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
diff --git a/feature/users/domain/src/commonMain/kotlin/org/timemates/app/users/validation/UserDescriptionValidator.kt b/feature/users/domain/src/commonMain/kotlin/org/timemates/app/users/validation/UserDescriptionValidator.kt
deleted file mode 100644
index 2fc7fcd..0000000
--- a/feature/users/domain/src/commonMain/kotlin/org/timemates/app/users/validation/UserDescriptionValidator.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package org.timemates.app.users.validation
-
-import org.timemates.app.foundation.validation.Validator
-import org.timemates.app.foundation.validation.unknownValidationFailure
-import org.timemates.app.users.validation.UserDescriptionValidator.Result
-import io.timemates.sdk.common.constructor.CreationFailure
-import io.timemates.sdk.users.profile.types.value.UserDescription
-
-class UserDescriptionValidator : Validator {
- override fun validate(input: String): Result {
- return UserDescription.create(input)
- .map { Result.Success }
- .getOrElse { throwable ->
- when (throwable) {
- is CreationFailure.SizeRangeFailure ->
- Result.SizeViolation
-
- else -> unknownValidationFailure(throwable)
- }
- }
- }
-
- sealed class Result {
- object Success : Result()
-
- object SizeViolation : Result()
- }
-}
\ No newline at end of file
diff --git a/feature/users/domain/src/commonMain/kotlin/org/timemates/app/users/validation/UserNameValidator.kt b/feature/users/domain/src/commonMain/kotlin/org/timemates/app/users/validation/UserNameValidator.kt
deleted file mode 100644
index 38a17a7..0000000
--- a/feature/users/domain/src/commonMain/kotlin/org/timemates/app/users/validation/UserNameValidator.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package org.timemates.app.users.validation
-
-import org.timemates.app.foundation.validation.Validator
-import org.timemates.app.foundation.validation.unknownValidationFailure
-import org.timemates.app.users.validation.UserNameValidator.Result
-import io.timemates.sdk.common.constructor.CreationFailure
-import io.timemates.sdk.users.profile.types.value.UserName
-
-class UserNameValidator : Validator {
- override fun validate(input: String): Result {
- return UserName.create(input)
- .map { Result.Success }
- .getOrElse { throwable ->
- when (throwable) {
- is CreationFailure.SizeRangeFailure ->
- Result.SizeViolation
-
- else -> unknownValidationFailure(throwable)
- }
- }
- }
-
- sealed class Result {
- object Success : Result()
-
- object SizeViolation : Result()
- }
-}
\ No newline at end of file
diff --git a/feature/users/domain/src/jvmTest/kotlin/org/timemates/app/users/usecases/EditUserUseCaseTest.kt b/feature/users/domain/src/jvmTest/kotlin/org/timemates/app/users/usecases/EditUserUseCaseTest.kt
index b7a3144..4053cee 100644
--- a/feature/users/domain/src/jvmTest/kotlin/org/timemates/app/users/usecases/EditUserUseCaseTest.kt
+++ b/feature/users/domain/src/jvmTest/kotlin/org/timemates/app/users/usecases/EditUserUseCaseTest.kt
@@ -2,14 +2,14 @@ package org.timemates.app.users.usecases
import io.mockk.coEvery
import io.mockk.mockk
+import kotlinx.coroutines.runBlocking
import org.timemates.app.users.repositories.AuthorizationsRepository
import org.timemates.app.users.repositories.UsersRepository
-import io.timemates.sdk.common.constructor.createOrThrow
-import io.timemates.sdk.common.types.Empty
-import io.timemates.sdk.users.profile.types.value.UserDescription
-import io.timemates.sdk.users.profile.types.value.UserId
-import io.timemates.sdk.users.profile.types.value.UserName
-import kotlinx.coroutines.runBlocking
+import org.timemates.sdk.common.constructor.createOrThrow
+import org.timemates.sdk.common.types.Empty
+import org.timemates.sdk.users.profile.types.value.UserDescription
+import org.timemates.sdk.users.profile.types.value.UserId
+import org.timemates.sdk.users.profile.types.value.UserName
import kotlin.test.Test
import kotlin.test.assertEquals
@@ -22,9 +22,9 @@ class EditUserUseCaseTest {
@Test
fun `execute with valid name and description should return Success result`() {
// GIVEN
- val userId = UserId.createOrThrow(0)
- val name = UserName.createOrThrow("John Doe")
- val description = UserDescription.createOrThrow("Some description")
+ val userId = UserId.factory.createOrThrow(0)
+ val name = UserName.factory.createOrThrow("John Doe")
+ val description = UserDescription.factory.createOrThrow("Some description")
coEvery { authorizations.getMe() } returns Result.success(userId)
coEvery { repository.editUser(name, description) } returns Result.success(Empty)
@@ -38,7 +38,7 @@ class EditUserUseCaseTest {
@Test
fun `execute with editUser() failure should return Failure result`() {
// GIVEN
- val userId = UserId.createOrThrow(1)
+ val userId = UserId.factory.createOrThrow(1)
val exception = Exception("Failed to edit user")
coEvery { authorizations.getMe() } returns Result.success(userId)
coEvery { repository.editUser(null, null) } returns Result.failure(exception)
diff --git a/feature/users/domain/src/jvmTest/kotlin/org/timemates/app/users/usecases/GetUserUseCaseTest.kt b/feature/users/domain/src/jvmTest/kotlin/org/timemates/app/users/usecases/GetUserUseCaseTest.kt
index 12befcf..cf24db2 100644
--- a/feature/users/domain/src/jvmTest/kotlin/org/timemates/app/users/usecases/GetUserUseCaseTest.kt
+++ b/feature/users/domain/src/jvmTest/kotlin/org/timemates/app/users/usecases/GetUserUseCaseTest.kt
@@ -2,16 +2,16 @@ package org.timemates.app.users.usecases
import io.mockk.coEvery
import io.mockk.mockk
-import org.timemates.app.users.repositories.UsersRepository
-import io.timemates.sdk.common.constructor.createOrThrow
-import io.timemates.sdk.common.exceptions.NotFoundException
-import io.timemates.sdk.users.profile.types.User
-import io.timemates.sdk.users.profile.types.value.UserDescription
-import io.timemates.sdk.users.profile.types.value.UserId
-import io.timemates.sdk.users.profile.types.value.UserName
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
+import org.timemates.app.users.repositories.UsersRepository
+import org.timemates.sdk.common.constructor.createOrThrow
+import org.timemates.sdk.common.exceptions.NotFoundException
+import org.timemates.sdk.users.profile.types.User
+import org.timemates.sdk.users.profile.types.value.UserDescription
+import org.timemates.sdk.users.profile.types.value.UserId
+import org.timemates.sdk.users.profile.types.value.UserName
import kotlin.test.Test
import kotlin.test.assertEquals
@@ -23,11 +23,11 @@ class GetUserUseCaseTest {
@Test
fun `execute with valid userId should return Success result`(): Unit = runTest {
// GIVEN
- val userId = UserId.createOrThrow(1)
+ val userId = UserId.factory.createOrThrow(1)
val user = User(
id = userId,
- name = UserName.createOrThrow("John"),
- description = UserDescription.createOrThrow("Description"),
+ name = UserName.factory.createOrThrow("John"),
+ description = UserDescription.factory.createOrThrow("Description"),
avatar = null,
emailAddress = null,
)
@@ -46,7 +46,7 @@ class GetUserUseCaseTest {
@Test
fun `execute with NotFoundException should return NotFound result`(): Unit = runTest {
// GIVEN
- val userId = UserId.createOrThrow(1)
+ val userId = UserId.factory.createOrThrow(1)
coEvery { repository.getUser(userId) } returns flowOf(Result.failure(NotFoundException("User not found")))
// WHEN
@@ -63,7 +63,7 @@ class GetUserUseCaseTest {
@Test
fun `execute with other exceptions should return Failure result`(): Unit = runTest {
// GIVEN
- val userId = UserId.createOrThrow(1)
+ val userId = UserId.factory.createOrThrow(1)
val exception = Exception("Failed to retrieve user")
coEvery { repository.getUser(userId) } returns flowOf(Result.failure(exception))
diff --git a/feature/users/domain/src/jvmTest/kotlin/org/timemates/app/users/usecases/GetUsersUseCaseTest.kt b/feature/users/domain/src/jvmTest/kotlin/org/timemates/app/users/usecases/GetUsersUseCaseTest.kt
index 9f49976..74932a5 100644
--- a/feature/users/domain/src/jvmTest/kotlin/org/timemates/app/users/usecases/GetUsersUseCaseTest.kt
+++ b/feature/users/domain/src/jvmTest/kotlin/org/timemates/app/users/usecases/GetUsersUseCaseTest.kt
@@ -2,16 +2,16 @@ package org.timemates.app.users.usecases
import io.mockk.coEvery
import io.mockk.mockk
-import org.timemates.app.users.repositories.UsersRepository
-import io.timemates.sdk.common.constructor.createOrThrow
-import io.timemates.sdk.common.exceptions.NotFoundException
-import io.timemates.sdk.users.profile.types.User
-import io.timemates.sdk.users.profile.types.value.UserDescription
-import io.timemates.sdk.users.profile.types.value.UserId
-import io.timemates.sdk.users.profile.types.value.UserName
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
+import org.timemates.app.users.repositories.UsersRepository
+import org.timemates.sdk.common.constructor.createOrThrow
+import org.timemates.sdk.common.exceptions.NotFoundException
+import org.timemates.sdk.users.profile.types.User
+import org.timemates.sdk.users.profile.types.value.UserDescription
+import org.timemates.sdk.users.profile.types.value.UserId
+import org.timemates.sdk.users.profile.types.value.UserName
import kotlin.test.Test
import kotlin.test.assertEquals
@@ -23,19 +23,19 @@ class GetUsersUseCaseTest {
@Test
fun `execute with valid userIds should return Success result`(): Unit = runTest {
// GIVEN
- val userIds = listOf(UserId.createOrThrow(1), UserId.createOrThrow(2))
+ val userIds = listOf(UserId.factory.createOrThrow(1), UserId.factory.createOrThrow(2))
val users = listOf(
User(
id = userIds[0],
- name = UserName.createOrThrow("John"),
- description = UserDescription.createOrThrow("Description 1"),
+ name = UserName.factory.createOrThrow("John"),
+ description = UserDescription.factory.createOrThrow("Description 1"),
emailAddress = null,
avatar = null,
),
User(
id = userIds[1],
- name = UserName.createOrThrow("Jane"),
- description = UserDescription.createOrThrow("Description 2"),
+ name = UserName.factory.createOrThrow("Jane"),
+ description = UserDescription.factory.createOrThrow("Description 2"),
emailAddress = null,
avatar = null,
)
@@ -56,7 +56,7 @@ class GetUsersUseCaseTest {
@Test
fun `execute with NotFoundException should return NotFound result`(): Unit = runTest {
// GIVEN
- val userIds = listOf(UserId.createOrThrow(1))
+ val userIds = listOf(UserId.factory.createOrThrow(1))
coEvery { repository.getUsers(userIds) } returns flowOf(Result.failure(NotFoundException("User not found")))
// WHEN
@@ -72,7 +72,7 @@ class GetUsersUseCaseTest {
@Test
fun `execute with other exceptions should return Failure result`(): Unit = runTest {
// GIVEN
- val userIds = listOf(UserId.createOrThrow(1))
+ val userIds = listOf(UserId.factory.createOrThrow(1))
val exception = Exception("Failed to retrieve users")
coEvery { repository.getUsers(userIds) } returns flowOf(Result.failure(exception))
diff --git a/feature/users/presentation/build.gradle.kts b/feature/users/presentation/build.gradle.kts
index 70e4397..1ec6704 100644
--- a/feature/users/presentation/build.gradle.kts
+++ b/feature/users/presentation/build.gradle.kts
@@ -4,10 +4,9 @@ plugins {
}
dependencies {
- commonMainImplementation(projects.feature.common.presentation)
+ commonMainImplementation(projects.core.ui)
commonMainImplementation(libs.timemates.sdk)
- commonMainImplementation(projects.foundation.mvi)
- commonMainImplementation(projects.styleSystem)
+ commonMainImplementation(projects.core.styleSystem)
commonMainImplementation(projects.feature.users.domain)
commonTestImplementation(projects.foundation.random)
diff --git a/foundation/mvi/build.gradle.kts b/foundation/mvi/build.gradle.kts
deleted file mode 100644
index 158c3bb..0000000
--- a/foundation/mvi/build.gradle.kts
+++ /dev/null
@@ -1,14 +0,0 @@
-plugins {
- id(libs.plugins.configurations.multiplatform.library.get().pluginId)
-}
-
-kotlin {
- jvm()
-
- explicitApi()
-}
-
-dependencies {
- commonMainApi(projects.foundation.viewmodel)
- commonMainImplementation(libs.kotlinx.coroutines)
-}
\ No newline at end of file
diff --git a/foundation/mvi/koin-compose/build.gradle.kts b/foundation/mvi/koin-compose/build.gradle.kts
deleted file mode 100644
index c3f2fb7..0000000
--- a/foundation/mvi/koin-compose/build.gradle.kts
+++ /dev/null
@@ -1,30 +0,0 @@
-plugins {
- id(libs.plugins.configurations.compose.multiplatform.get().pluginId)
-}
-
-kotlin {
- jvm()
- androidTarget()
-
- sourceSets {
- androidMain {
- dependencies {
- implementation(libs.androidx.compose.viewmodel)
- implementation(libs.koin.androidx.compose)
- }
- }
- }
-}
-
-dependencies {
- commonMainImplementation(compose.ui)
- commonMainImplementation(compose.runtime)
- commonMainImplementation(libs.koin.core)
- commonMainImplementation(libs.koin.compose)
-
- commonMainImplementation(projects.foundation.mvi)
-}
-
-android {
- namespace = "org.timemates.app.mvi.compose"
-}
\ No newline at end of file
diff --git a/foundation/mvi/koin-compose/src/androidMain/AndroidManifest.xml b/foundation/mvi/koin-compose/src/androidMain/AndroidManifest.xml
deleted file mode 100644
index a1f5a90..0000000
--- a/foundation/mvi/koin-compose/src/androidMain/AndroidManifest.xml
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
diff --git a/foundation/mvi/koin-compose/src/androidMain/kotlin/org/timemates/app/mvi/compose/stateMachine.kt b/foundation/mvi/koin-compose/src/androidMain/kotlin/org/timemates/app/mvi/compose/stateMachine.kt
deleted file mode 100644
index fd5f68a..0000000
--- a/foundation/mvi/koin-compose/src/androidMain/kotlin/org/timemates/app/mvi/compose/stateMachine.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-@file:Suppress("USELESS_CAST")
-
-package org.timemates.app.mvi.compose
-
-import androidx.compose.runtime.Composable
-import androidx.lifecycle.ViewModelStoreOwner
-import androidx.lifecycle.viewmodel.CreationExtras
-import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
-import org.timemates.androidx.viewmodel.ViewModel
-import org.timemates.app.foundation.mvi.*
-import org.koin.androidx.compose.defaultExtras
-import org.koin.androidx.viewmodel.resolveViewModel
-import org.koin.compose.LocalKoinScope
-import org.koin.core.annotation.KoinInternalApi
-import org.koin.core.parameter.ParametersDefinition
-import org.koin.core.qualifier.Qualifier
-import org.koin.core.scope.Scope
-
-/**
- * Creates and returns an instance of the specified state machine using the provided factory.
- *
- * @param TSM The reified type of the state machine.
- * @param TState The type of the state in the state machine.
- * @param TEvent The type of the events in the state machine.
- * @param TEffect The type of the effects in the state machine.
- * @param factory The factory to create the state machine instance.
- * @return The created instance of the state machine.
- */
-@Composable
-actual inline fun > stateMachine(
- noinline parameters: ParametersDefinition?,
-) = koinVM(parameters = parameters)
-
-@OptIn(KoinInternalApi::class)
-@PublishedApi
-@Composable
-internal inline fun koinVM(
- qualifier: Qualifier? = null,
- viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
- "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
- },
- key: String? = null,
- extras: CreationExtras = defaultExtras(viewModelStoreOwner),
- scope: Scope = LocalKoinScope.current,
- noinline parameters: ParametersDefinition? = null,
-): T {
- return resolveViewModel(
- T::class, viewModelStoreOwner.viewModelStore, key, extras, qualifier, scope, parameters
- )
-}
diff --git a/foundation/mvi/koin-compose/src/jvmMain/kotlin/org/timemates/app/mvi/compose/stateMachine.kt b/foundation/mvi/koin-compose/src/jvmMain/kotlin/org/timemates/app/mvi/compose/stateMachine.kt
deleted file mode 100644
index 8faa02a..0000000
--- a/foundation/mvi/koin-compose/src/jvmMain/kotlin/org/timemates/app/mvi/compose/stateMachine.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package org.timemates.app.mvi.compose
-
-import androidx.compose.runtime.Composable
-import org.timemates.app.foundation.mvi.StateMachine
-import org.koin.compose.koinInject
-import org.koin.core.parameter.ParametersDefinition
-
-@Composable
-actual inline fun > stateMachine(
- noinline parameters: ParametersDefinition?,
-): TSM = koinInject(
- parameters = parameters,
-)
\ No newline at end of file
diff --git a/foundation/mvi/src/commonMain/kotlin/org/timemates/app/foundation/mvi/Middleware.kt b/foundation/mvi/src/commonMain/kotlin/org/timemates/app/foundation/mvi/Middleware.kt
deleted file mode 100644
index 4674a22..0000000
--- a/foundation/mvi/src/commonMain/kotlin/org/timemates/app/foundation/mvi/Middleware.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-package org.timemates.app.foundation.mvi
-
-/**
- * Middleware is an interface that represents a component in the Model-View-Intent (MVI) architecture
- * responsible for intercepting effects and performing side effects based on those effects.
- *
- * @param TState The type representing the UI state.
- * @param TEffect The type representing effects for the UI.
- */
-public interface Middleware {
- /**
- * Handles the given effect and performs any necessary side effects.
- *
- * @param effect The effect to handle.
- * @param state The current state.
- */
- public fun onEffect(effect: TEffect, state: TState): TState
-}
-
diff --git a/foundation/mvi/src/commonMain/kotlin/org/timemates/app/foundation/mvi/Reducer.kt b/foundation/mvi/src/commonMain/kotlin/org/timemates/app/foundation/mvi/Reducer.kt
deleted file mode 100644
index 44f9875..0000000
--- a/foundation/mvi/src/commonMain/kotlin/org/timemates/app/foundation/mvi/Reducer.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-package org.timemates.app.foundation.mvi
-
-import kotlinx.coroutines.CoroutineScope
-
-/**
- * A reducer is responsible for updating the state based on events and triggering effects.
- *
- * @param TState The type representing the UI state.
- * @param TEvent The type representing events from the UI.
- * @param TEffect The type representing effects to the UI.
- */
-public interface Reducer {
-
- /**
- * Reduces the current state based on the given event and triggers effects, if necessary.
- *
- * @param state The current state of the UI.
- * @param event The event to be processed.
- * @param sendEffect The function to send effects to the UI.
- * Call `sendEffect(effect)` to send an effect to be handled by the UI.
- * The effect will be delivered asynchronously.
- * Note: Ensure proper handling and validation of effects to avoid issues like
- * infinite loops or unexpected behaviors.
- * @return The new state after processing the event.
- */
- public fun ReducerScope.reduce(
- state: TState,
- event: TEvent,
- ): TState
-}
-
-public fun Reducer.reduce(
- state: TState,
- event: TEvent,
- machineScope: CoroutineScope,
- sendEffect: (TEffect) -> Unit,
-): TState {
- return ReducerScope(sendEffect, machineScope).reduce(state, event)
-}
-
-/**
- * Reducer's Scope with additional functionality and information.
- *
- * @param sendEffect sends effect to the UI. It also can be received by a [Middleware].
- * @param machineScope CoroutineScope that is linked to the StateMachine.
- */
-public data class ReducerScope(
- val sendEffect: (TEffect) -> Unit,
- val machineScope: CoroutineScope,
-)
diff --git a/foundation/mvi/src/commonMain/kotlin/org/timemates/app/foundation/mvi/StateMachine.kt b/foundation/mvi/src/commonMain/kotlin/org/timemates/app/foundation/mvi/StateMachine.kt
deleted file mode 100644
index 4655769..0000000
--- a/foundation/mvi/src/commonMain/kotlin/org/timemates/app/foundation/mvi/StateMachine.kt
+++ /dev/null
@@ -1,49 +0,0 @@
-package org.timemates.app.foundation.mvi
-
-import org.timemates.androidx.viewmodel.ViewModel
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.channels.ReceiveChannel
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.update
-
-public abstract class StateMachine(
- protected val initState: TState,
- private val reducer: Reducer,
- private val middlewares: List> = emptyList(),
-) : ViewModel(), StateStore {
- private val _state: MutableStateFlow by lazy { MutableStateFlow(initState) }
- private val _effects: Channel = Channel(Channel.UNLIMITED)
-
- /**
- * Represents the current state of the UI.
- */
- public final override val state: StateFlow by ::_state
-
- /**
- * Represents the channel for emitting UI effects.
- */
- public val effects: ReceiveChannel by ::_effects
-
- private val sendEffect: (TEffect) -> Unit = { effect ->
- middlewares.forEach { middleware -> updateState { middleware.onEffect(effect, it) } }
- _effects.trySend(effect)
- }
-
- private val reducerScope = ReducerScope(sendEffect, coroutineScope)
-
- /**
- * Processes an event from UI.
- *
- * @param event The event to be processed.
- */
- public fun dispatchEvent(event: TEvent) {
- with(reducer) {
- updateState { reducerScope.reduce(it, event) }
- }
- }
-
- private fun updateState(action: (TState) -> TState) {
- _state.update { action(it) }
- }
-}
diff --git a/foundation/mvi/src/commonMain/kotlin/org/timemates/app/foundation/mvi/StateStore.kt b/foundation/mvi/src/commonMain/kotlin/org/timemates/app/foundation/mvi/StateStore.kt
deleted file mode 100644
index c0af385..0000000
--- a/foundation/mvi/src/commonMain/kotlin/org/timemates/app/foundation/mvi/StateStore.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package org.timemates.app.foundation.mvi
-
-import kotlinx.coroutines.flow.StateFlow
-
-/**
- * An interface representing a state store.
- *
- * @param TState The type of the state.
- */
-public interface StateStore {
- /**
- * The state flow representing the current state.
- */
- public val state: StateFlow
-}
diff --git a/foundation/mvi/src/commonMain/kotlin/org/timemates/app/foundation/mvi/UiEffect.kt b/foundation/mvi/src/commonMain/kotlin/org/timemates/app/foundation/mvi/UiEffect.kt
deleted file mode 100644
index 822de82..0000000
--- a/foundation/mvi/src/commonMain/kotlin/org/timemates/app/foundation/mvi/UiEffect.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package org.timemates.app.foundation.mvi
-
-/**
- * One-time actions for UI layer. Like side effects.
- */
-public interface UiEffect
\ No newline at end of file
diff --git a/foundation/mvi/src/commonMain/kotlin/org/timemates/app/foundation/mvi/UiEvent.kt b/foundation/mvi/src/commonMain/kotlin/org/timemates/app/foundation/mvi/UiEvent.kt
deleted file mode 100644
index 8d53eda..0000000
--- a/foundation/mvi/src/commonMain/kotlin/org/timemates/app/foundation/mvi/UiEvent.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package org.timemates.app.foundation.mvi
-
-/**
- * Interface-marker for events that sends by UI.
- */
-public interface UiEvent
\ No newline at end of file
diff --git a/foundation/mvi/src/commonMain/kotlin/org/timemates/app/foundation/mvi/UiState.kt b/foundation/mvi/src/commonMain/kotlin/org/timemates/app/foundation/mvi/UiState.kt
deleted file mode 100644
index 7af5b53..0000000
--- a/foundation/mvi/src/commonMain/kotlin/org/timemates/app/foundation/mvi/UiState.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package org.timemates.app.foundation.mvi
-
-/**
- * Interface-marker for states that used by UI.
- */
-public interface UiState
-
-public object EmptyState : UiState
\ No newline at end of file
diff --git a/foundation/viewmodel/build.gradle.kts b/foundation/viewmodel/build.gradle.kts
deleted file mode 100644
index 42f8eab..0000000
--- a/foundation/viewmodel/build.gradle.kts
+++ /dev/null
@@ -1,30 +0,0 @@
-plugins {
- id(libs.plugins.configurations.multiplatform.library.get().pluginId)
-}
-
-kotlin {
- jvm()
- androidTarget()
-
- sourceSets {
- androidMain {
- dependencies {
- api(libs.androidx.lifecycle)
- }
- }
- }
-}
-
-android {
- compileSdk = libs.versions.android.target.get().toInt()
-
- defaultConfig {
- minSdk = libs.versions.android.min.get().toInt()
- }
-
- namespace = "org.timemates.androidx.viewmodel"
-}
-
-dependencies {
- commonMainImplementation(libs.kotlinx.coroutines)
-}
\ No newline at end of file
diff --git a/foundation/viewmodel/src/androidMain/kotlin/org/timemates/androidx/viewmodel/ViewModel.kt b/foundation/viewmodel/src/androidMain/kotlin/org/timemates/androidx/viewmodel/ViewModel.kt
deleted file mode 100644
index ed29e75..0000000
--- a/foundation/viewmodel/src/androidMain/kotlin/org/timemates/androidx/viewmodel/ViewModel.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-package org.timemates.androidx.viewmodel
-
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import kotlinx.coroutines.CoroutineScope
-
-@Suppress("ACTUAL_CLASSIFIER_MUST_HAVE_THE_SAME_SUPERTYPES_AS_NON_FINAL_EXPECT_CLASSIFIER_WARNING")
-actual abstract class ViewModel : ViewModel() {
- /**
- * The coroutine scope associated with this ViewModel.
- *
- * The `viewModelScope` is a [CoroutineScope] provided by the Android Jetpack's ViewModel library.
- * It is used to launch coroutines that are scoped to the lifecycle of the ViewModel.
- * Any coroutines launched in this scope will automatically be canceled when the ViewModel is cleared or destroyed.
- *
- * @see [ViewModel]
- * @see [CoroutineScope]
- */
- actual val coroutineScope: CoroutineScope by ::viewModelScope
-}
\ No newline at end of file
diff --git a/foundation/viewmodel/src/commonMain/kotlin/org/timemates/androidx/viewmodel/ViewModel.kt b/foundation/viewmodel/src/commonMain/kotlin/org/timemates/androidx/viewmodel/ViewModel.kt
deleted file mode 100644
index 9631c62..0000000
--- a/foundation/viewmodel/src/commonMain/kotlin/org/timemates/androidx/viewmodel/ViewModel.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package org.timemates.androidx.viewmodel
-
-import kotlinx.coroutines.CoroutineScope
-
-/**
- * Abstract class representing a ViewModel.
- */
-expect abstract class ViewModel() {
- /**
- * The coroutine scope associated with this ViewModel.
- * Automatically cancels when ViewModel is not within user scope.
- */
- val coroutineScope: CoroutineScope
-}
diff --git a/foundation/viewmodel/src/jvmMain/kotlin/org/timemates/androidx/viewmodel/ViewModel.kt b/foundation/viewmodel/src/jvmMain/kotlin/org/timemates/androidx/viewmodel/ViewModel.kt
deleted file mode 100644
index 7f2ed12..0000000
--- a/foundation/viewmodel/src/jvmMain/kotlin/org/timemates/androidx/viewmodel/ViewModel.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package org.timemates.androidx.viewmodel
-
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-
-actual abstract class ViewModel {
- /**
- * The coroutine scope associated with this ViewModel.
- * Automatically cancels when ViewModel is not within user scope.
- */
- actual val coroutineScope: CoroutineScope by lazy {
- CoroutineScope(
- Job() + Dispatchers.Default
- )
- }
-}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 6f910cb..19a1530 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,11 +1,11 @@
[versions]
# Kotlin
-kotlin = "1.9.21"
-kotlinx-coroutines = "1.7.3"
-kotlinx-serialization = "1.6.0"
+kotlin = "1.9.22"
+kotlinx-coroutines = "1.8.0"
+kotlinx-serialization = "1.6.3"
# Libraries
-ktor = "2.3.7"
+ktor = "2.3.8"
# Testing
jupiter = "5.8.2"
@@ -14,38 +14,42 @@ androidx-test-ext-junit = "1.1.5"
espresso-core = "3.5.1"
# UI
-androidGradlePlugin = "8.2.0"
+androidGradlePlugin = "8.2.2"
androidComposeVersion = "2.6.2"
-jetpackComposeCompilerVersion = "1.5.6"
-jetbrainsCompose = "1.5.11"
+jetpackComposeCompilerVersion = "1.5.8"
+jetbrainsCompose = "1.6.0"
material = "1.11.0"
-timeMatesSdk = "dev-8d542c313773fbda8127ed8e1dc2d20a70107b19"
+timeMatesSdk = "dev-2e6f1991053b561042f0f4ff10b82592f46137fb"
-ksp = "1.9.21-1.0.16"
+ksp = "1.9.22-1.0.17"
+flowmvi = "2.5.0-alpha06"
# Koin
koin = "3.5.3"
-koin-annotations = "1.3.0"
+koin-annotations = "1.3.1"
-decompose = "2.2.1"
+decompose = "3.0.0-alpha08"
sqldelight = "2.0.1"
moko-resources = "0.23.0"
-jna = "5.13.0"
core-ktx = "1.12.0"
-credentials = "1.0.0"
+credentials = "1.0.1"
securityCryptoKtx = "1.1.0-alpha06"
# Build constants
android-target = "34"
android-min = "24"
+essenty = "1.3.0"
+
[libraries]
# KotlinX libraries
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
+kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
kotlinx-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
-kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.require = "0.4.0" }
+kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.require = "0.5.0" }
+kotlinx-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.require = "0.3.7" }
# Ktor
ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" }
@@ -58,6 +62,7 @@ ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx
ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negotiation", version.ref = "ktor" }
ktor-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
+ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
# Logging
@@ -69,26 +74,26 @@ cache4k = { module = "io.github.reactivecircus.cache4k:cache4k", version.require
# AndroidX Compose
androidx-compose-ui = { module = "androidx.compose.ui:ui", version.ref = "androidComposeVersion" }
androidx-compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "androidComposeVersion" }
-androidx-compose-material3 = { module = "androidx.compose.material3:material3", version.require = "1.1.2" }
+androidx-compose-material3 = { module = "androidx.compose.material3:material3", version.require = "1.2.1" }
androidx-compose-icons = { module = "androidx.compose.material:material-icons-core", version.ref = "androidComposeVersion" }
androidx-compose-extendedIcons = { module = "androidx.compose.material:material-icons-extended", version.ref = "androidComposeVersion" }
androidx-compose-activity = { module = "androidx.activity:activity-compose", version.require = "1.8.2" }
-androidx-compose-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidComposeVersion" }
-androidx-compose-tooling = { module = "androidx.compose.ui:ui-tooling", version.require = "1.5.4" }
-androidx-compose-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.require = "1.5.4" }
+androidx-compose-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.require = "2.8.0-alpha02" }
+androidx-compose-tooling = { module = "androidx.compose.ui:ui-tooling", version.require = "1.6.3" }
+androidx-compose-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.require = "1.6.3" }
compose-accompanist-systemUiController = { module = "com.google.accompanist:accompanist-systemuicontroller", version.require = "0.31.6-rc" }
# AndroidX (other)
androidx-security-crypto-ktx = { module = "androidx.security:security-crypto-ktx", version.ref = "securityCryptoKtx" }
-androidx-lifecycle = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.require = "2.6.2" }
+androidx-lifecycle = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.require = "2.7.0" }
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.require = "1.7.0-alpha03" }
# TimeMates SDK
-timemates-sdk = { module = "io.timemates:sdk", version.ref = "timeMatesSdk" }
-timemates-engine-rsocket = { module = "io.timemates:rsocket-engine", version.ref = "timeMatesSdk" }
-timemates-credentials-manager = { module = "io.timemates.credentials:credentials-manager", version.ref = "credentials" }
-timemates-credentials-manager-android = { module = "io.timemates.credentials:credentials-manager-android", version.ref = "credentials" }
+timemates-sdk = { module = "org.timemates.sdk:sdk", version.ref = "timeMatesSdk" }
+timemates-engine-rsocket = { module = "org.timemates.sdk:rsocket-engine", version.ref = "timeMatesSdk" }
+timemates-credentials-manager = { module = "org.timemates.credentials:credentials-manager", version.ref = "credentials" }
+timemates-credentials-manager-android = { module = "org.timemates.credentials:credentials-manager-android", version.ref = "credentials" }
# Koin
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
@@ -97,7 +102,7 @@ koin-ksp-compiler = { module = "io.insert-koin:koin-ksp-compiler", version.ref =
# Decompose
decompose = { module = "com.arkivanov.decompose:decompose", version.ref = "decompose" }
-decompose-jetbrains-compose = { module = "com.arkivanov.decompose:extensions-compose-jetbrains", version.ref = "decompose" }
+decompose-jetbrains-compose = { module = "com.arkivanov.decompose:extensions-compose", version.ref = "decompose" }
koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.require = "3.4.5" }
koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
koin-compose = { module = "io.insert-koin:koin-compose", version.require = "1.0.3" }
@@ -123,10 +128,6 @@ sqldelight-jvm-driver = { module = "app.cash.sqldelight:sqlite-driver", version.
sqldelight-android-driver = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }
sqldelight-runtime = { module = "app.cash.sqldelight:runtime", version.ref = "sqldelight" }
-# JNA
-jna-core = { module = "net.java.dev.jna:jna", version.ref = "jna" }
-jna-platform = { module = "net.java.dev.jna:jna-platform", version.ref = "jna" }
-
# Android Multidex
android-multidex = { module = "androidx.multidex:multidex", version.require = "2.0.1" }
@@ -143,6 +144,27 @@ androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version
espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" }
mockk = { group = "io.mockk", name = "mockk", version.require = "1.13.5" }
+## Essenty
+essenty-instanceKeeper = { module = "com.arkivanov.essenty:instance-keeper", version.ref = "essenty" }
+essenty-lifecycle = { module = "com.arkivanov.essenty:lifecycle", version.ref = "essenty" }
+
+# FlowMVI
+flowmvi-core = { module = "pro.respawn.flowmvi:core", version.ref = "flowmvi" }
+# Test DSL
+flowmvi-test = { module = "pro.respawn.flowmvi:test", version.ref = "flowmvi" }
+# Compose multiplatform
+flowmvi-compose = { module = "pro.respawn.flowmvi:compose", version.ref = "flowmvi" }
+# Common android
+flowmvi-android = { module = "pro.respawn.flowmvi:android", version.ref = "flowmvi" }
+# View-based android
+flowmvi-view = { module = "pro.respawn.flowmvi:android-view", version.ref = "flowmvi" }
+# Multiplatform state preservation
+flowmvi-savedstate = { module = "pro.respawn.flowmvi:savedstate", version.ref = "flowmvi" }
+# Remote debugging client
+flowmvi-debugger-client = { module = "pro.respawn.flowmvi:debugger-plugin", version.ref = "flowmvi" }
+# Essenty (Decompose) integration
+flowmvi-essenty = { module = "pro.respawn.flowmvi:essenty", version.ref = "flowmvi" }
+flowmvi-essenty-compose = { module = "pro.respawn.flowmvi:essenty-compose", version.ref = "flowmvi" }
# Material
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
@@ -150,6 +172,24 @@ material = { group = "com.google.android.material", name = "material", version.r
# Compose resources
libres-compose = { module = "io.github.skeptick.libres:libres-compose", version.require = "1.2.2" }
+[bundles]
+# Presentation layer preset libraries
+presentation = [
+ "decompose",
+ "decompose-jetbrains-compose",
+ "flowmvi-core",
+ "flowmvi-compose",
+ "flowmvi-essenty",
+ "flowmvi-essenty-compose",
+ "kotlinx-coroutines",
+ "kotlinx-immutable",
+]
+# Dependencies module preset libraries
+koinDeps = [
+ "koin-core",
+ "koin-annotations",
+]
+
[plugins]
# Build conventions
configurations-multiplatform-library = { id = "multiplatform-library-convention", version.require = "SNAPSHOT" }
@@ -178,7 +218,7 @@ kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" }
# Android Application Plugin
-android-application = { id = "com.android.library", version.ref = "androidGradlePlugin" }
+android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
# Compose Multiplatform Plugin
compose-multiplatform = { id = "org.jetbrains.compose", version.ref = "jetbrainsCompose" }
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
deleted file mode 100644
index e708b1c..0000000
Binary files a/gradle/wrapper/gradle-wrapper.jar and /dev/null differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index db9a6b8..17655d0 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/platforms/android/build.gradle.kts b/platform/android/build.gradle.kts
similarity index 86%
rename from platforms/android/build.gradle.kts
rename to platform/android/build.gradle.kts
index fad067c..b798a29 100644
--- a/platforms/android/build.gradle.kts
+++ b/platform/android/build.gradle.kts
@@ -1,3 +1,5 @@
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
@@ -63,7 +65,7 @@ dependencies {
implementation(libs.sqldelight.android.driver)
- implementation(projects.feature.common.domain)
+ implementation(projects.core.ui)
implementation(projects.feature.users.data.database)
implementation(projects.feature.authorization.data.database)
@@ -75,6 +77,11 @@ dependencies {
implementation(libs.androidx.compose.activity)
- implementation(projects.platforms.common)
+ implementation(projects.platform.common)
implementation(libs.androidx.security.crypto.ktx)
+
+ implementation(libs.essenty.instanceKeeper)
+ implementation(libs.essenty.lifecycle)
+
+ implementation(libs.androidx.lifecycle)
}
\ No newline at end of file
diff --git a/platforms/android/src/main/AndroidManifest.xml b/platform/android/src/main/AndroidManifest.xml
similarity index 100%
rename from platforms/android/src/main/AndroidManifest.xml
rename to platform/android/src/main/AndroidManifest.xml
diff --git a/platforms/android/src/main/ic_launcher-playstore.png b/platform/android/src/main/ic_launcher-playstore.png
similarity index 100%
rename from platforms/android/src/main/ic_launcher-playstore.png
rename to platform/android/src/main/ic_launcher-playstore.png
diff --git a/platforms/android/src/main/kotlin/org/timemates/app/AppActivity.kt b/platform/android/src/main/kotlin/org/timemates/app/AppActivity.kt
similarity index 82%
rename from platforms/android/src/main/kotlin/org/timemates/app/AppActivity.kt
rename to platform/android/src/main/kotlin/org/timemates/app/AppActivity.kt
index ebca0e4..2a5dc13 100644
--- a/platforms/android/src/main/kotlin/org/timemates/app/AppActivity.kt
+++ b/platform/android/src/main/kotlin/org/timemates/app/AppActivity.kt
@@ -14,6 +14,8 @@ import com.arkivanov.decompose.defaultComponentContext
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.pop
import com.google.accompanist.systemuicontroller.rememberSystemUiController
+import org.timemates.app.feature.common.providable.LocalTimeProvider
+import org.timemates.app.foundation.time.SystemUTCTimeProvider
import org.timemates.app.navigation.LocalComponentContext
import org.timemates.app.navigation.Screen
import org.timemates.app.navigation.TimeMatesAppEntry
@@ -37,11 +39,14 @@ class AppActivity : ComponentActivity() {
}
}
- CompositionLocalProvider(LocalComponentContext provides componentContent) {
+ CompositionLocalProvider(
+ LocalComponentContext provides componentContent,
+ LocalTimeProvider provides SystemUTCTimeProvider(),
+ ) {
AppTheme {
- Box(modifier = Modifier.fillMaxSize()) {
- systemUiController.setSystemBarsColor(AppTheme.colors.background)
+ systemUiController.setSystemBarsColor(AppTheme.colors.background)
+ Box(modifier = Modifier.fillMaxSize()) {
TimeMatesAppEntry(
navigation = navigation,
navigateToAuthorization = TimeMatesApplication.onAuthFailedChannel,
diff --git a/platforms/android/src/main/kotlin/org/timemates/app/TimeMatesApplication.kt b/platform/android/src/main/kotlin/org/timemates/app/TimeMatesApplication.kt
similarity index 91%
rename from platforms/android/src/main/kotlin/org/timemates/app/TimeMatesApplication.kt
rename to platform/android/src/main/kotlin/org/timemates/app/TimeMatesApplication.kt
index c02545c..1805ae1 100644
--- a/platforms/android/src/main/kotlin/org/timemates/app/TimeMatesApplication.kt
+++ b/platform/android/src/main/kotlin/org/timemates/app/TimeMatesApplication.kt
@@ -9,9 +9,9 @@ import org.timemates.app.common.initializeAppDependencies
import org.timemates.app.credentials.AndroidEncryptedPrefsCredentials
import org.timemates.app.foundation.time.SystemUTCTimeProvider
import org.timemates.app.users.data.database.TimeMatesUsers
-import io.timemates.credentials.CredentialsStorage
+import org.timemates.credentials.CredentialsStorage
import io.timemates.data.database.TimeMatesAuthorizations
-import io.timemates.sdk.common.exceptions.UnauthorizedException
+import org.timemates.sdk.common.exceptions.UnauthorizedException
import kotlinx.coroutines.channels.Channel
class TimeMatesApplication : MultiDexApplication() {
@@ -46,6 +46,5 @@ class TimeMatesApplication : MultiDexApplication() {
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
- MultiDex.install(this)
}
}
diff --git a/platforms/android/src/main/kotlin/org/timemates/app/credentials/AndroidEncryptedPrefsCredentials.kt b/platform/android/src/main/kotlin/org/timemates/app/credentials/AndroidEncryptedPrefsCredentials.kt
similarity index 97%
rename from platforms/android/src/main/kotlin/org/timemates/app/credentials/AndroidEncryptedPrefsCredentials.kt
rename to platform/android/src/main/kotlin/org/timemates/app/credentials/AndroidEncryptedPrefsCredentials.kt
index 97335d0..d7bedb7 100644
--- a/platforms/android/src/main/kotlin/org/timemates/app/credentials/AndroidEncryptedPrefsCredentials.kt
+++ b/platform/android/src/main/kotlin/org/timemates/app/credentials/AndroidEncryptedPrefsCredentials.kt
@@ -2,7 +2,7 @@ package org.timemates.app.credentials
import android.content.Context
import android.content.SharedPreferences
-import io.timemates.credentials.CredentialsStorage
+import org.timemates.credentials.CredentialsStorage
/**
* Android encrypted shared preferences implementation of credentials manager
diff --git a/platforms/android/src/main/kotlin/org/timemates/app/credentials/createEncryptedSharedPreferences.kt b/platform/android/src/main/kotlin/org/timemates/app/credentials/createEncryptedSharedPreferences.kt
similarity index 100%
rename from platforms/android/src/main/kotlin/org/timemates/app/credentials/createEncryptedSharedPreferences.kt
rename to platform/android/src/main/kotlin/org/timemates/app/credentials/createEncryptedSharedPreferences.kt
diff --git a/platforms/android/src/main/res/drawable/ic_launcher_foreground.xml b/platform/android/src/main/res/drawable/ic_launcher_foreground.xml
similarity index 100%
rename from platforms/android/src/main/res/drawable/ic_launcher_foreground.xml
rename to platform/android/src/main/res/drawable/ic_launcher_foreground.xml
diff --git a/platforms/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/platform/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
similarity index 100%
rename from platforms/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
rename to platform/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
diff --git a/platforms/android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/platform/android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
similarity index 100%
rename from platforms/android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
rename to platform/android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
diff --git a/platforms/android/src/main/res/mipmap-hdpi/ic_launcher.webp b/platform/android/src/main/res/mipmap-hdpi/ic_launcher.webp
similarity index 100%
rename from platforms/android/src/main/res/mipmap-hdpi/ic_launcher.webp
rename to platform/android/src/main/res/mipmap-hdpi/ic_launcher.webp
diff --git a/platforms/android/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/platform/android/src/main/res/mipmap-hdpi/ic_launcher_round.webp
similarity index 100%
rename from platforms/android/src/main/res/mipmap-hdpi/ic_launcher_round.webp
rename to platform/android/src/main/res/mipmap-hdpi/ic_launcher_round.webp
diff --git a/platforms/android/src/main/res/mipmap-mdpi/ic_launcher.webp b/platform/android/src/main/res/mipmap-mdpi/ic_launcher.webp
similarity index 100%
rename from platforms/android/src/main/res/mipmap-mdpi/ic_launcher.webp
rename to platform/android/src/main/res/mipmap-mdpi/ic_launcher.webp
diff --git a/platforms/android/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/platform/android/src/main/res/mipmap-mdpi/ic_launcher_round.webp
similarity index 100%
rename from platforms/android/src/main/res/mipmap-mdpi/ic_launcher_round.webp
rename to platform/android/src/main/res/mipmap-mdpi/ic_launcher_round.webp
diff --git a/platforms/android/src/main/res/mipmap-xhdpi/ic_launcher.webp b/platform/android/src/main/res/mipmap-xhdpi/ic_launcher.webp
similarity index 100%
rename from platforms/android/src/main/res/mipmap-xhdpi/ic_launcher.webp
rename to platform/android/src/main/res/mipmap-xhdpi/ic_launcher.webp
diff --git a/platforms/android/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/platform/android/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
similarity index 100%
rename from platforms/android/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
rename to platform/android/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
diff --git a/platforms/android/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/platform/android/src/main/res/mipmap-xxhdpi/ic_launcher.webp
similarity index 100%
rename from platforms/android/src/main/res/mipmap-xxhdpi/ic_launcher.webp
rename to platform/android/src/main/res/mipmap-xxhdpi/ic_launcher.webp
diff --git a/platforms/android/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/platform/android/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
similarity index 100%
rename from platforms/android/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
rename to platform/android/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
diff --git a/platforms/android/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/platform/android/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
similarity index 100%
rename from platforms/android/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
rename to platform/android/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
diff --git a/platforms/android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/platform/android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
similarity index 100%
rename from platforms/android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
rename to platform/android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
diff --git a/platforms/android/src/main/res/values/ic_launcher_background.xml b/platform/android/src/main/res/values/ic_launcher_background.xml
similarity index 100%
rename from platforms/android/src/main/res/values/ic_launcher_background.xml
rename to platform/android/src/main/res/values/ic_launcher_background.xml
diff --git a/platforms/common/build.gradle.kts b/platform/common/build.gradle.kts
similarity index 76%
rename from platforms/common/build.gradle.kts
rename to platform/common/build.gradle.kts
index c867088..ca7592f 100644
--- a/platforms/common/build.gradle.kts
+++ b/platform/common/build.gradle.kts
@@ -13,14 +13,14 @@ dependencies {
commonMainImplementation(libs.ktor.client.cio)
- commonMainImplementation(projects.feature.system.dependencies)
+ commonMainImplementation(projects.feature.splash.dependencies)
commonMainImplementation(libs.kotlinx.coroutines)
commonMainImplementation(libs.sqldelight.runtime)
commonMainApi(projects.foundation.time)
- commonMainImplementation(projects.feature.common.domain)
+ commonMainImplementation(projects.core.ui)
commonMainImplementation(projects.feature.authorization.dependencies)
commonMainImplementation(projects.feature.timers.dependencies)
@@ -31,13 +31,10 @@ dependencies {
jvmMainApi(libs.sqldelight.jvm.driver)
androidMainApi(libs.sqldelight.android.driver)
- commonMainApi(projects.navigation)
- commonMainApi(projects.styleSystem)
+ commonMainApi(projects.core.navigation)
+ commonMainApi(projects.core.styleSystem)
commonMainApi(libs.timemates.credentials.manager)
- commonMainImplementation(projects.foundation.mvi)
- commonMainImplementation(projects.foundation.mvi.koinCompose)
-
commonMainImplementation(projects.feature.authorization.domain)
}
\ No newline at end of file
diff --git a/platforms/common/src/commonMain/kotlin/org/timemates/app/common/App.kt b/platform/common/src/commonMain/kotlin/org/timemates/app/common/App.kt
similarity index 60%
rename from platforms/common/src/commonMain/kotlin/org/timemates/app/common/App.kt
rename to platform/common/src/commonMain/kotlin/org/timemates/app/common/App.kt
index 92f1b58..ac66744 100644
--- a/platforms/common/src/commonMain/kotlin/org/timemates/app/common/App.kt
+++ b/platform/common/src/commonMain/kotlin/org/timemates/app/common/App.kt
@@ -5,15 +5,21 @@ import androidx.compose.runtime.CompositionLocalProvider
import com.arkivanov.decompose.ComponentContext
import org.timemates.app.navigation.LocalComponentContext
import org.timemates.app.navigation.TimeMatesAppEntry
-import io.timemates.sdk.common.exceptions.UnauthorizedException
+import org.timemates.sdk.common.exceptions.UnauthorizedException
import kotlinx.coroutines.channels.ReceiveChannel
+import org.timemates.app.feature.common.providable.LocalTimeProvider
+import org.timemates.app.foundation.time.TimeProvider
@Composable
fun App(
+ timeProvider: TimeProvider,
componentContext: ComponentContext,
onAuthFailed: ReceiveChannel,
) {
- CompositionLocalProvider(LocalComponentContext provides componentContext) {
+ CompositionLocalProvider(
+ LocalComponentContext provides componentContext,
+ LocalTimeProvider provides timeProvider,
+ ) {
TimeMatesAppEntry(
navigateToAuthorization = onAuthFailed,
)
diff --git a/platforms/common/src/commonMain/kotlin/org/timemates/app/common/Dependencies.kt b/platform/common/src/commonMain/kotlin/org/timemates/app/common/Dependencies.kt
similarity index 58%
rename from platforms/common/src/commonMain/kotlin/org/timemates/app/common/Dependencies.kt
rename to platform/common/src/commonMain/kotlin/org/timemates/app/common/Dependencies.kt
index 8b0141e..ec06372 100644
--- a/platforms/common/src/commonMain/kotlin/org/timemates/app/common/Dependencies.kt
+++ b/platform/common/src/commonMain/kotlin/org/timemates/app/common/Dependencies.kt
@@ -2,34 +2,25 @@ package org.timemates.app.common
import app.cash.sqldelight.async.coroutines.awaitCreate
import app.cash.sqldelight.db.SqlDriver
-import io.timemates.api.rsocket.RSocketTimeMatesRequestsEngine
-import org.timemates.app.authorization.dependencies.AuthorizationDataModule
-import org.timemates.app.authorization.dependencies.screens.AfterStartModule
-import org.timemates.app.authorization.dependencies.screens.ConfigureAccountModule
-import org.timemates.app.authorization.dependencies.screens.ConfirmAuthorizationModule
-import org.timemates.app.authorization.dependencies.screens.InitialAuthorizationModule
-import org.timemates.app.authorization.dependencies.screens.NewAccountInfoModule
-import org.timemates.app.authorization.dependencies.screens.StartAuthorizationModule
-import org.timemates.app.feature.common.handler.OnAuthorizationFailedHandler
-import org.timemates.app.feature.system.dependencies.SystemDataModule
-import org.timemates.app.feature.system.dependencies.screens.StartupModule
-import org.timemates.app.foundation.time.TimeProvider
-import org.timemates.app.timers.dependencies.screens.TimerCreationModule
-import org.timemates.app.timers.dependencies.screens.TimerSettingsModule
-import org.timemates.app.timers.dependencies.screens.TimersListModule
-import io.timemates.credentials.CredentialsStorage
import io.timemates.data.database.TimeMatesAuthorizations
-import io.timemates.sdk.common.engine.TimeMatesRequestsEngine
-import io.timemates.sdk.common.exceptions.UnauthorizedException
-import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.runBlocking
import org.koin.core.context.startKoin
-import org.koin.ksp.generated.module
import org.koin.core.qualifier.qualifier
import org.koin.dsl.module
+import org.koin.ksp.generated.module
+import org.timemates.api.rsocket.RSocketTimeMatesRequestsEngine
+import org.timemates.app.authorization.dependencies.AuthorizationModule
+import org.timemates.app.feature.common.failures.OnAuthorizationFailedHandler
+import org.timemates.app.feature.splash.dependencies.SplashModule
+import org.timemates.app.foundation.time.TimeProvider
+import org.timemates.app.timers.dependencies.TimersModule
+import org.timemates.app.users.dependencies.UsersModule
+import org.timemates.credentials.CredentialsStorage
+import org.timemates.sdk.common.engine.TimeMatesRequestsEngine
+import org.timemates.sdk.common.exceptions.UnauthorizedException
/**
* Initializes the application dependencies using Koin.
@@ -61,7 +52,9 @@ fun initializeAppDependencies(
startKoin {
val appModule = module {
single {
- RSocketTimeMatesRequestsEngine(coroutineScope = CoroutineScope(Dispatchers.IO))
+ RSocketTimeMatesRequestsEngine(
+ coroutineScope = CoroutineScope(Dispatchers.IO),
+ )
}
single(qualifier("authorization")) {
@@ -88,18 +81,10 @@ fun initializeAppDependencies(
modules(
appModule,
- InitialAuthorizationModule().module,
- AuthorizationDataModule().module,
- ConfirmAuthorizationModule().module,
- StartAuthorizationModule().module,
- AfterStartModule().module,
- NewAccountInfoModule().module,
- ConfigureAccountModule().module,
- TimersListModule().module,
- TimerCreationModule().module,
- TimerSettingsModule().module,
- SystemDataModule().module,
- StartupModule().module,
+ SplashModule().module,
+ AuthorizationModule().module,
+ TimersModule().module,
+ UsersModule().module,
)
}
}
\ No newline at end of file
diff --git a/platforms/desktop/build.gradle.kts b/platform/desktop/build.gradle.kts
similarity index 85%
rename from platforms/desktop/build.gradle.kts
rename to platform/desktop/build.gradle.kts
index 17df46c..60ba0d6 100644
--- a/platforms/desktop/build.gradle.kts
+++ b/platform/desktop/build.gradle.kts
@@ -10,7 +10,7 @@ dependencies {
implementation(libs.koin.core)
- implementation(projects.feature.common.domain)
+ implementation(projects.core.ui)
implementation(libs.timemates.sdk)
implementation(libs.timemates.engine.rsocket)
@@ -26,9 +26,11 @@ dependencies {
implementation(projects.foundation.time)
- implementation(projects.styleSystem)
+ implementation(projects.core.styleSystem)
- implementation(projects.platforms.common)
+ implementation(projects.platform.common)
+
+ implementation(libs.kotlinx.coroutines.swing)
}
java {
diff --git a/platforms/desktop/src/main/kotlin/org/timemates/app/AppConstants.kt b/platform/desktop/src/main/kotlin/org/timemates/app/AppConstants.kt
similarity index 100%
rename from platforms/desktop/src/main/kotlin/org/timemates/app/AppConstants.kt
rename to platform/desktop/src/main/kotlin/org/timemates/app/AppConstants.kt
diff --git a/platforms/desktop/src/main/kotlin/org/timemates/app/Main.kt b/platform/desktop/src/main/kotlin/org/timemates/app/Main.kt
similarity index 81%
rename from platforms/desktop/src/main/kotlin/org/timemates/app/Main.kt
rename to platform/desktop/src/main/kotlin/org/timemates/app/Main.kt
index 41e31bd..abbd009 100644
--- a/platforms/desktop/src/main/kotlin/org/timemates/app/Main.kt
+++ b/platform/desktop/src/main/kotlin/org/timemates/app/Main.kt
@@ -1,11 +1,12 @@
package org.timemates.app
import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
+import kotlinx.coroutines.channels.Channel
import org.timemates.app.common.initializeAppDependencies
import org.timemates.app.foundation.time.SystemUTCTimeProvider
-import io.timemates.credentials.DesktopCredentialsStorage
-import io.timemates.sdk.common.exceptions.UnauthorizedException
-import kotlinx.coroutines.channels.Channel
+import org.timemates.app.storage.AppDirectory
+import org.timemates.credentials.DesktopCredentialsStorage
+import org.timemates.sdk.common.exceptions.UnauthorizedException
import kotlin.io.path.pathString
fun main() {
diff --git a/platforms/desktop/src/main/kotlin/org/timemates/app/Tray.kt b/platform/desktop/src/main/kotlin/org/timemates/app/Tray.kt
similarity index 100%
rename from platforms/desktop/src/main/kotlin/org/timemates/app/Tray.kt
rename to platform/desktop/src/main/kotlin/org/timemates/app/Tray.kt
diff --git a/platforms/desktop/src/main/kotlin/org/timemates/app/UI.kt b/platform/desktop/src/main/kotlin/org/timemates/app/UI.kt
similarity index 82%
rename from platforms/desktop/src/main/kotlin/org/timemates/app/UI.kt
rename to platform/desktop/src/main/kotlin/org/timemates/app/UI.kt
index 1a2009a..3b8c40b 100644
--- a/platforms/desktop/src/main/kotlin/org/timemates/app/UI.kt
+++ b/platform/desktop/src/main/kotlin/org/timemates/app/UI.kt
@@ -1,6 +1,5 @@
package org.timemates.app
-import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
@@ -8,18 +7,10 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Timer
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
-import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.draw.shadow
-import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.focus.onFocusEvent
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
@@ -29,16 +20,14 @@ import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import com.arkivanov.decompose.DefaultComponentContext
import com.arkivanov.decompose.ExperimentalDecomposeApi
-import com.arkivanov.decompose.extensions.compose.jetbrains.lifecycle.LifecycleController
-import com.arkivanov.decompose.extensions.compose.jetbrains.stack.animation.fade
+import com.arkivanov.decompose.extensions.compose.lifecycle.LifecycleController
import com.arkivanov.essenty.lifecycle.LifecycleRegistry
+import com.arkivanov.essenty.statekeeper.StateKeeperDispatcher
+import kotlinx.coroutines.channels.Channel
import org.timemates.app.common.App
+import org.timemates.app.foundation.time.SystemUTCTimeProvider
import org.timemates.app.style.system.theme.AppTheme
-import io.timemates.sdk.common.exceptions.UnauthorizedException
-import kotlinx.coroutines.channels.Channel
-import java.awt.Frame
-import java.awt.event.FocusEvent
-import java.awt.event.FocusListener
+import org.timemates.sdk.common.exceptions.UnauthorizedException
import java.awt.event.WindowEvent
import java.awt.event.WindowListener
import javax.swing.SwingUtilities
@@ -46,7 +35,11 @@ import javax.swing.SwingUtilities
@OptIn(ExperimentalDecomposeApi::class)
fun startUi(authorizationFailedChannel: Channel) {
val lifecycle = LifecycleRegistry()
- val rootComponentContext = DefaultComponentContext(lifecycle = lifecycle)
+ val stateKeeper = StateKeeperDispatcher()
+ val rootComponentContext = DefaultComponentContext(
+ lifecycle = lifecycle,
+ stateKeeper = stateKeeper,
+ )
application {
val (isAppVisible, setIsAppVisible) = remember { mutableStateOf(false) }
@@ -89,7 +82,8 @@ fun startUi(authorizationFailedChannel: Channel) {
AppTheme(useDarkTheme = false) {
App(
componentContext = rootComponentContext,
- authorizationFailedChannel,
+ onAuthFailed = authorizationFailedChannel,
+ timeProvider = SystemUTCTimeProvider(),
)
}
}
diff --git a/platforms/desktop/src/main/kotlin/org/timemates/app/AppDirectory.kt b/platform/desktop/src/main/kotlin/org/timemates/app/storage/AppDirectory.kt
similarity index 97%
rename from platforms/desktop/src/main/kotlin/org/timemates/app/AppDirectory.kt
rename to platform/desktop/src/main/kotlin/org/timemates/app/storage/AppDirectory.kt
index 760df50..5c55eca 100644
--- a/platforms/desktop/src/main/kotlin/org/timemates/app/AppDirectory.kt
+++ b/platform/desktop/src/main/kotlin/org/timemates/app/storage/AppDirectory.kt
@@ -1,4 +1,4 @@
-package org.timemates.app
+package org.timemates.app.storage
import java.nio.file.Path
import java.nio.file.attribute.PosixFilePermission
diff --git a/style-system/src/commonMain/libres/fonts/Inter-Black.ttf b/platform/desktop/src/main/resources/fonts/Inter-Black.ttf
similarity index 100%
rename from style-system/src/commonMain/libres/fonts/Inter-Black.ttf
rename to platform/desktop/src/main/resources/fonts/Inter-Black.ttf
diff --git a/style-system/src/commonMain/libres/fonts/Inter-Bold.ttf b/platform/desktop/src/main/resources/fonts/Inter-Bold.ttf
similarity index 100%
rename from style-system/src/commonMain/libres/fonts/Inter-Bold.ttf
rename to platform/desktop/src/main/resources/fonts/Inter-Bold.ttf
diff --git a/style-system/src/commonMain/libres/fonts/Inter-ExtraBold.ttf b/platform/desktop/src/main/resources/fonts/Inter-ExtraBold.ttf
similarity index 100%
rename from style-system/src/commonMain/libres/fonts/Inter-ExtraBold.ttf
rename to platform/desktop/src/main/resources/fonts/Inter-ExtraBold.ttf
diff --git a/style-system/src/commonMain/libres/fonts/Inter-ExtraLight.ttf b/platform/desktop/src/main/resources/fonts/Inter-ExtraLight.ttf
similarity index 100%
rename from style-system/src/commonMain/libres/fonts/Inter-ExtraLight.ttf
rename to platform/desktop/src/main/resources/fonts/Inter-ExtraLight.ttf
diff --git a/style-system/src/commonMain/libres/fonts/Inter-Light.ttf b/platform/desktop/src/main/resources/fonts/Inter-Light.ttf
similarity index 100%
rename from style-system/src/commonMain/libres/fonts/Inter-Light.ttf
rename to platform/desktop/src/main/resources/fonts/Inter-Light.ttf
diff --git a/style-system/src/commonMain/libres/fonts/Inter-Medium.ttf b/platform/desktop/src/main/resources/fonts/Inter-Medium.ttf
similarity index 100%
rename from style-system/src/commonMain/libres/fonts/Inter-Medium.ttf
rename to platform/desktop/src/main/resources/fonts/Inter-Medium.ttf
diff --git a/style-system/src/commonMain/libres/fonts/Inter-Regular.ttf b/platform/desktop/src/main/resources/fonts/Inter-Regular.ttf
similarity index 100%
rename from style-system/src/commonMain/libres/fonts/Inter-Regular.ttf
rename to platform/desktop/src/main/resources/fonts/Inter-Regular.ttf
diff --git a/style-system/src/commonMain/libres/fonts/Inter-SemiBold.ttf b/platform/desktop/src/main/resources/fonts/Inter-SemiBold.ttf
similarity index 100%
rename from style-system/src/commonMain/libres/fonts/Inter-SemiBold.ttf
rename to platform/desktop/src/main/resources/fonts/Inter-SemiBold.ttf
diff --git a/style-system/src/commonMain/libres/fonts/Inter-Thin.ttf b/platform/desktop/src/main/resources/fonts/Inter-Thin.ttf
similarity index 100%
rename from style-system/src/commonMain/libres/fonts/Inter-Thin.ttf
rename to platform/desktop/src/main/resources/fonts/Inter-Thin.ttf
diff --git a/platforms/desktop/src/main/resources/images/app-icon-rounded.png b/platform/desktop/src/main/resources/images/app-icon-rounded.png
similarity index 100%
rename from platforms/desktop/src/main/resources/images/app-icon-rounded.png
rename to platform/desktop/src/main/resources/images/app-icon-rounded.png
diff --git a/platforms/desktop/src/main/resources/images/app-tray-icon.png b/platform/desktop/src/main/resources/images/app-tray-icon.png
similarity index 100%
rename from platforms/desktop/src/main/resources/images/app-tray-icon.png
rename to platform/desktop/src/main/resources/images/app-tray-icon.png
diff --git a/style-system/src/commonMain/libres/images/app_icon.png b/platform/desktop/src/main/resources/images/app_icon.png
similarity index 100%
rename from style-system/src/commonMain/libres/images/app_icon.png
rename to platform/desktop/src/main/resources/images/app_icon.png
diff --git a/style-system/src/commonMain/libres/images/confirm_authorization_info_image.svg b/platform/desktop/src/main/resources/images/confirm_authorization_info_image.svg
similarity index 100%
rename from style-system/src/commonMain/libres/images/confirm_authorization_info_image.svg
rename to platform/desktop/src/main/resources/images/confirm_authorization_info_image.svg
diff --git a/platforms/desktop/src/main/resources/images/empty_list_image.svg b/platform/desktop/src/main/resources/images/empty_list_image.svg
similarity index 100%
rename from platforms/desktop/src/main/resources/images/empty_list_image.svg
rename to platform/desktop/src/main/resources/images/empty_list_image.svg
diff --git a/style-system/src/commonMain/libres/images/initial_screen_image.svg b/platform/desktop/src/main/resources/images/initial_screen_image.svg
similarity index 100%
rename from style-system/src/commonMain/libres/images/initial_screen_image.svg
rename to platform/desktop/src/main/resources/images/initial_screen_image.svg
diff --git a/style-system/src/commonMain/libres/images/new_account_info_image.svg b/platform/desktop/src/main/resources/images/new_account_info_image.svg
similarity index 100%
rename from style-system/src/commonMain/libres/images/new_account_info_image.svg
rename to platform/desktop/src/main/resources/images/new_account_info_image.svg
diff --git a/preview/build.gradle.kts b/preview/build.gradle.kts
index 98d8a40..75287a9 100644
--- a/preview/build.gradle.kts
+++ b/preview/build.gradle.kts
@@ -27,21 +27,17 @@ android {
}
dependencies {
- implementation(libs.core.ktx)
- implementation(libs.androidx.appcompat)
- implementation(libs.material)
- implementation(libs.timemates.sdk)
- implementation(libs.kotlinx.datetime)
-
- implementation(projects.styleSystem)
+ implementation(projects.core.ui)
debugImplementation(libs.androidx.compose.tooling)
implementation(libs.androidx.compose.preview)
implementation(libs.androidx.compose.material3)
- implementation(projects.localization)
- implementation(projects.localization.compose)
+ implementation(projects.core.localization)
+ implementation(projects.core.localization.compose)
implementation(projects.feature.authorization.presentation)
implementation(projects.feature.timers.presentation)
+
+ implementation(libs.decompose.jetbrains.compose)
}
diff --git a/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/authorization/AfterStartScreen.kt b/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/authorization/AfterStartScreen.kt
index ea78237..20d2960 100644
--- a/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/authorization/AfterStartScreen.kt
+++ b/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/authorization/AfterStartScreen.kt
@@ -2,9 +2,9 @@ package org.timemates.app.preview.feature.feature.authorization
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
-import org.timemates.app.preview.feature.statemachine.fakeStateMachine
import org.timemates.app.authorization.ui.afterstart.AfterStartScreen
-import org.timemates.app.foundation.mvi.EmptyState
+import org.timemates.app.authorization.ui.afterstart.mvi.AfterStartScreenComponent.State
+import org.timemates.app.preview.feature.mvi.fakeMvi
import org.timemates.app.style.system.theme.AppTheme
@Preview
@@ -12,7 +12,7 @@ import org.timemates.app.style.system.theme.AppTheme
internal fun AfterStartScreenPreview() {
AppTheme {
AfterStartScreen(
- stateMachine = fakeStateMachine(EmptyState),
+ mvi = fakeMvi(State),
navigateToConfirmation = {},
navigateToStart = {}
)
diff --git a/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/authorization/ConfigureNewAccountScreen.kt b/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/authorization/ConfigureNewAccountScreen.kt
index 212dc1f..bdb8372 100644
--- a/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/authorization/ConfigureNewAccountScreen.kt
+++ b/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/authorization/ConfigureNewAccountScreen.kt
@@ -2,9 +2,9 @@ package org.timemates.app.preview.feature.feature.authorization
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
-import org.timemates.app.preview.feature.statemachine.fakeStateMachine
import org.timemates.app.authorization.ui.configure_account.ConfigureAccountScreen
-import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountStateMachine.State
+import org.timemates.app.authorization.ui.configure_account.mvi.ConfigureAccountScreenComponent.State
+import org.timemates.app.preview.feature.mvi.fakeMvi
import org.timemates.app.style.system.theme.AppTheme
@Preview
@@ -12,7 +12,7 @@ import org.timemates.app.style.system.theme.AppTheme
internal fun ConfigureNewAccountScreenPreview() {
AppTheme {
ConfigureAccountScreen(
- stateMachine = fakeStateMachine(State()),
+ mvi = fakeMvi(State()),
navigateToHome = {},
onBack = {},
)
diff --git a/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/authorization/ConfirmationScreen.kt b/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/authorization/ConfirmationScreen.kt
index 769c36f..4fa781d 100644
--- a/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/authorization/ConfirmationScreen.kt
+++ b/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/authorization/ConfirmationScreen.kt
@@ -2,9 +2,9 @@ package org.timemates.app.preview.feature.feature.authorization
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
-import org.timemates.app.preview.feature.statemachine.fakeStateMachine
import org.timemates.app.authorization.ui.confirmation.ConfirmAuthorizationScreen
-import org.timemates.app.authorization.ui.confirmation.mvi.ConfirmAuthorizationStateMachine.State
+import org.timemates.app.authorization.ui.confirmation.mvi.ConfirmAuthorizationScreenComponent.State
+import org.timemates.app.preview.feature.mvi.fakeMvi
import org.timemates.app.style.system.theme.AppTheme
@Preview
@@ -12,7 +12,7 @@ import org.timemates.app.style.system.theme.AppTheme
internal fun ConfirmationScreenPreview() {
AppTheme {
ConfirmAuthorizationScreen(
- stateMachine = fakeStateMachine(State()),
+ mvi = fakeMvi(State()),
onBack = {},
navigateToConfiguring = {},
navigateToHome = {},
diff --git a/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/authorization/InitialAuthorizationScreen.kt b/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/authorization/InitialAuthorizationScreen.kt
index 8be1201..87bc19e 100644
--- a/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/authorization/InitialAuthorizationScreen.kt
+++ b/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/authorization/InitialAuthorizationScreen.kt
@@ -2,9 +2,9 @@ package org.timemates.app.preview.feature.feature.authorization
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
-import org.timemates.app.preview.feature.statemachine.fakeStateMachine
import org.timemates.app.authorization.ui.initial_authorization.InitialAuthorizationScreen
-import org.timemates.app.foundation.mvi.EmptyState
+import org.timemates.app.authorization.ui.initial_authorization.mvi.InitialAuthorizationComponent
+import org.timemates.app.preview.feature.mvi.fakeMvi
import org.timemates.app.style.system.theme.AppTheme
@Preview
@@ -12,7 +12,7 @@ import org.timemates.app.style.system.theme.AppTheme
fun InitialScreenPreview() {
AppTheme {
InitialAuthorizationScreen(
- stateMachine = fakeStateMachine(EmptyState),
+ mvi = fakeMvi(InitialAuthorizationComponent.State),
navigateToStartAuthorization = {},
)
}
diff --git a/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/authorization/NewAccountInfoScreen.kt b/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/authorization/NewAccountInfoScreen.kt
index 16b7bc4..02143ae 100644
--- a/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/authorization/NewAccountInfoScreen.kt
+++ b/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/authorization/NewAccountInfoScreen.kt
@@ -2,9 +2,9 @@ package org.timemates.app.preview.feature.feature.authorization
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
-import org.timemates.app.preview.feature.statemachine.fakeStateMachine
import org.timemates.app.authorization.ui.new_account_info.NewAccountInfoScreen
-import org.timemates.app.foundation.mvi.EmptyState
+import org.timemates.app.authorization.ui.new_account_info.mvi.NewAccountInfoScreenComponent.State
+import org.timemates.app.preview.feature.mvi.fakeMvi
import org.timemates.app.style.system.theme.AppTheme
@Preview
@@ -12,7 +12,7 @@ import org.timemates.app.style.system.theme.AppTheme
internal fun NewAccountInfoScreenPreview() {
AppTheme {
NewAccountInfoScreen(
- stateMachine = fakeStateMachine(EmptyState),
+ mvi = fakeMvi(State),
navigateToConfigure = {},
navigateToStart = {}
)
diff --git a/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/authorization/StartAuthorizationScreen.kt b/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/authorization/StartAuthorizationScreen.kt
index 6e5a370..6befeb6 100644
--- a/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/authorization/StartAuthorizationScreen.kt
+++ b/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/authorization/StartAuthorizationScreen.kt
@@ -3,14 +3,14 @@ package org.timemates.app.preview.feature.feature.authorization
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import org.timemates.app.authorization.ui.start.StartAuthorizationScreen
-import org.timemates.app.authorization.ui.start.mvi.StartAuthorizationStateMachine.State
+import org.timemates.app.authorization.ui.start.mvi.StartAuthorizationComponent
+import org.timemates.app.preview.feature.mvi.fakeMvi
import org.timemates.app.style.system.theme.AppTheme
-import org.timemates.app.preview.feature.statemachine.fakeStateMachine
@Preview
@Composable
internal fun StartAuthorizationScreenPreview() {
AppTheme {
- StartAuthorizationScreen(stateMachine = fakeStateMachine(State()), onNavigateToConfirmation = {})
+ StartAuthorizationScreen(mvi = fakeMvi(StartAuthorizationComponent.State()), onNavigateToConfirmation = {})
}
}
\ No newline at end of file
diff --git a/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/timers/TimerCreationScreen.kt b/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/timers/TimerCreationScreen.kt
index b3f1e91..b375602 100644
--- a/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/timers/TimerCreationScreen.kt
+++ b/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/timers/TimerCreationScreen.kt
@@ -2,17 +2,17 @@ package org.timemates.app.preview.feature.feature.timers
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
-import org.timemates.app.preview.feature.statemachine.fakeStateMachine
+import org.timemates.app.preview.feature.mvi.fakeMvi
import org.timemates.app.style.system.theme.AppTheme
import org.timemates.app.timers.ui.timer_creation.TimerCreationScreen
-import org.timemates.app.timers.ui.timer_creation.mvi.TimerCreationStateMachine
+import org.timemates.app.timers.ui.timer_creation.mvi.TimerCreationScreenComponent
@Preview
@Composable
internal fun TimerCreationScreenPreview() {
AppTheme {
TimerCreationScreen(
- stateMachine = fakeStateMachine(TimerCreationStateMachine.State()),
+ mvi = fakeMvi(TimerCreationScreenComponent.State()),
navigateToTimersScreen = {},
)
}
diff --git a/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/timers/TimerListScreen.kt b/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/timers/TimerListScreen.kt
index a96113c..63d7566 100644
--- a/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/timers/TimerListScreen.kt
+++ b/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/timers/TimerListScreen.kt
@@ -2,17 +2,17 @@ package org.timemates.app.preview.feature.feature.timers
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
-import org.timemates.app.preview.feature.statemachine.fakeStateMachine
+import org.timemates.app.preview.feature.mvi.fakeMvi
import org.timemates.app.style.system.theme.AppTheme
import org.timemates.app.timers.ui.timers_list.TimersListScreen
-import org.timemates.app.timers.ui.timers_list.mvi.TimersListStateMachine
+import org.timemates.app.timers.ui.timers_list.mvi.TimersListScreenComponent
@Preview
@Composable
internal fun TimerListScreenPreview() {
AppTheme {
TimersListScreen(
- stateMachine = fakeStateMachine(TimersListStateMachine.State()),
+ mvi = fakeMvi(TimersListScreenComponent.State()),
navigateToSetting = {},
navigateToTimerCreationScreen = {},
navigateToTimer = {},
diff --git a/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/timers/TimerSettingsScreen.kt b/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/timers/TimerSettingsScreen.kt
index 28b34f6..4400d6d 100644
--- a/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/timers/TimerSettingsScreen.kt
+++ b/preview/src/main/kotlin/org/timemates/app/preview/feature/feature/timers/TimerSettingsScreen.kt
@@ -2,17 +2,17 @@ package org.timemates.app.preview.feature.feature.timers
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
-import org.timemates.app.preview.feature.statemachine.fakeStateMachine
+import org.timemates.app.preview.feature.mvi.fakeMvi
import org.timemates.app.style.system.theme.AppTheme
import org.timemates.app.timers.ui.settings.TimerSettingsScreen
-import org.timemates.app.timers.ui.settings.mvi.TimerSettingsStateMachine.State
+import org.timemates.app.timers.ui.settings.mvi.TimerSettingsScreenComponent.State
@Preview
@Composable
internal fun TimerSettingsScreenPreview() {
AppTheme {
TimerSettingsScreen(
- stateMachine = fakeStateMachine(State()),
+ mvi = fakeMvi(State()),
navigateToTimersScreen = {},
)
}
diff --git a/preview/src/main/kotlin/org/timemates/app/preview/feature/mvi/fakeMviComponent.kt b/preview/src/main/kotlin/org/timemates/app/preview/feature/mvi/fakeMviComponent.kt
new file mode 100644
index 0000000..6873025
--- /dev/null
+++ b/preview/src/main/kotlin/org/timemates/app/preview/feature/mvi/fakeMviComponent.kt
@@ -0,0 +1,20 @@
+package org.timemates.app.preview.feature.mvi
+
+import com.arkivanov.decompose.ComponentContext
+import com.arkivanov.decompose.DefaultComponentContext
+import com.arkivanov.essenty.lifecycle.Lifecycle
+import com.arkivanov.essenty.lifecycle.LifecycleRegistry
+import org.timemates.app.feature.common.MVI
+import pro.respawn.flowmvi.api.MVIAction
+import pro.respawn.flowmvi.api.MVIIntent
+import pro.respawn.flowmvi.api.MVIState
+import pro.respawn.flowmvi.api.Store
+import pro.respawn.flowmvi.dsl.store
+
+fun fakeMvi(
+ state: TState,
+): MVI {
+ return object : MVI, ComponentContext by DefaultComponentContext(LifecycleRegistry(initialState = Lifecycle.State.STARTED)) {
+ override val store: Store = store(initial = state) {}
+ }
+}
\ No newline at end of file
diff --git a/preview/src/main/kotlin/org/timemates/app/preview/feature/statemachine/fakeStateMachine.kt b/preview/src/main/kotlin/org/timemates/app/preview/feature/statemachine/fakeStateMachine.kt
deleted file mode 100644
index e998249..0000000
--- a/preview/src/main/kotlin/org/timemates/app/preview/feature/statemachine/fakeStateMachine.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-package org.timemates.app.preview.feature.statemachine
-
-import org.timemates.app.foundation.mvi.Reducer
-import org.timemates.app.foundation.mvi.ReducerScope
-import org.timemates.app.foundation.mvi.StateMachine
-import org.timemates.app.foundation.mvi.UiEffect
-import org.timemates.app.foundation.mvi.UiEvent
-import org.timemates.app.foundation.mvi.UiState
-
-internal fun fakeStateMachine(
- state: TState,
-): StateMachine {
- val reducer = object : Reducer {
- override fun ReducerScope.reduce(state: TState, event: TEvent): TState {
- return state
- }
- }
-
- return object : StateMachine(state, reducer, listOf()) {}
-}
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index c710f8d..c8cb82b 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -9,11 +9,13 @@ pluginManagement {
}
dependencyResolutionManagement {
- repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
+ repositoriesMode = RepositoriesMode.PREFER_PROJECT
+
repositories {
mavenCentral()
google()
- maven("https://maven.timemates.io")
+ maven("https://maven.timemates.org/releases")
+ maven("https://maven.timemates.org/dev")
}
}
@@ -22,17 +24,15 @@ rootProject.name = "timemates-app"
includeBuild("build-conventions")
include(
- ":localization",
- ":localization:compose",
+ ":core:localization",
+ ":core:localization:compose",
)
include(
- ":style-system",
+ ":core:style-system",
)
include(
- ":foundation:mvi",
- ":foundation:mvi:koin-compose",
":foundation:viewmodel",
":foundation:random",
":foundation:validation",
@@ -44,7 +44,7 @@ include(
)
include(
- ":navigation",
+ ":core:navigation",
)
include(
@@ -52,21 +52,21 @@ include(
)
include(
- ":platforms:desktop",
- ":platforms:android",
- ":platforms:common",
+ ":platform:desktop",
+ ":platform:android",
+ ":platform:common",
)
include(
- ":feature:common:domain",
- ":feature:common:presentation",
+ ":core:types:serializable",
+ ":core:ui",
)
include(
- ":feature:system:domain",
- ":feature:system:presentation",
- ":feature:system:adapters",
- ":feature:system:dependencies",
+ ":feature:splash:domain",
+ ":feature:splash:presentation",
+ ":feature:splash:adapters",
+ ":feature:splash:dependencies",
)
include(
diff --git a/stability-config.txt b/stability-config.txt
new file mode 100644
index 0000000..01dc368
--- /dev/null
+++ b/stability-config.txt
@@ -0,0 +1,12 @@
+org.timemates.sdk.**
+org.timemates.app.foundation.**
+org.timemates.app.timers.ui.**
+org.timemates.app.users.ui.**
+org.timemates.app.authorization.ui.**
+pro.respawn.flowmvi.api.MVIIntent
+pro.respawn.flowmvi.api.MVIState
+pro.respawn.flowmvi.api.MVIAction
+pro.respawn.flowmvi.api.Store
+pro.respawn.flowmvi.api.Container
+pro.respawn.flowmvi.api.ImmutableStore
+pro.respawn.flowmvi.dsl.LambdaIntent
\ No newline at end of file