diff --git a/build-logic/src/main/kotlin/android-app-convention.gradle.kts b/build-logic/src/main/kotlin/android-app-convention.gradle.kts index bbe26ca..db2151b 100644 --- a/build-logic/src/main/kotlin/android-app-convention.gradle.kts +++ b/build-logic/src/main/kotlin/android-app-convention.gradle.kts @@ -40,7 +40,7 @@ android { compose = true } composeOptions { - kotlinCompilerExtensionVersion = "1.4.0" + kotlinCompilerExtensionVersion = "1.4.2" } packagingOptions { resources { diff --git a/build-logic/src/main/kotlin/android-base-convention.gradle.kts b/build-logic/src/main/kotlin/android-base-convention.gradle.kts index 24e057f..81fc26e 100644 --- a/build-logic/src/main/kotlin/android-base-convention.gradle.kts +++ b/build-logic/src/main/kotlin/android-base-convention.gradle.kts @@ -5,7 +5,7 @@ configure { compileSdkVersion(33) defaultConfig { - minSdk = 24 + minSdk = 21 targetSdk = 33 } } diff --git a/mvi-core/src/commonMain/kotlin/net/humans/kmm/mvi/ReduxEngine.kt b/mvi-core/src/commonMain/kotlin/net/humans/kmm/mvi/ReduxEngine.kt index 52a3f44..9dd661f 100644 --- a/mvi-core/src/commonMain/kotlin/net/humans/kmm/mvi/ReduxEngine.kt +++ b/mvi-core/src/commonMain/kotlin/net/humans/kmm/mvi/ReduxEngine.kt @@ -10,7 +10,7 @@ class ReduxEngine( val input: SendChannel, val output: StateFlow, ) { - infix fun send(msg: M) { + fun send(msg: M) { input.trySend(msg).isSuccess } } diff --git a/mvi-sample/domain/src/commonMain/kotlin/net/humans/kmm/mvi/sample/domain/CommissionCalculatorEffectHandler.kt b/mvi-sample/domain/src/commonMain/kotlin/net/humans/kmm/mvi/sample/domain/CommissionCalculatorEffectHandler.kt index bc50183..cd5bcc0 100644 --- a/mvi-sample/domain/src/commonMain/kotlin/net/humans/kmm/mvi/sample/domain/CommissionCalculatorEffectHandler.kt +++ b/mvi-sample/domain/src/commonMain/kotlin/net/humans/kmm/mvi/sample/domain/CommissionCalculatorEffectHandler.kt @@ -6,6 +6,7 @@ import net.humans.kmm.mvi.sample.domain.CommissionCalculatorRedux.Effect import net.humans.kmm.mvi.sample.domain.CommissionCalculatorRedux.Message import net.humans.kmm.mvi.sample.domain.model.usd import net.humans.kmm.mvi.sample.domain.usecase.CalculateCashbackAndCommissionInput +import net.humans.kmm.mvi.sample.domain.usecase.CalculateCashbackAndCommissionResult import net.humans.kmm.mvi.sample.domain.usecase.CalculateCashbackAndCommissionUseCase import net.humans.kmm.mvi.sample.domain.usecase.GetBalanceUseCase import net.humans.kmm.mvi.sample.domain.usecase.impl.RandomCalculateCashbackAndCommissionUseCase @@ -17,16 +18,7 @@ class CommissionCalculatorEffectHandler( RandomCalculateCashbackAndCommissionUseCase() ) : CoroutineEffectHandler { override suspend fun handle(eff: Effect): Message = when (eff) { - is Effect.CalculateCommissionAndCashback -> - calculateCashbackAndCommissionUseCase.execute( - input = CalculateCashbackAndCommissionInput(inputAmount = eff.inputAmount) - ).let { result -> - Message.UpdateCommissionAndCashback( - commission = result.commission, - cashback = result.cashback, - ) - } - + is Effect.CalculateCommissionAndCashback -> calculateCommissionAndCashback(eff) Effect.Initialize -> Message.UpdateState( balance = getBalanceUseCase.execute().balance, inputAmount = BigDecimal.ZERO.usd, @@ -34,4 +26,23 @@ class CommissionCalculatorEffectHandler( cashback = BigDecimal.ZERO.usd, ) } + + private suspend fun calculateCommissionAndCashback( + eff: Effect.CalculateCommissionAndCashback + ): Message { + val input = CalculateCashbackAndCommissionInput( + balance = eff.balance, + inputAmount = eff.inputAmount + ) + return when (val result = calculateCashbackAndCommissionUseCase.execute(input = input)) { + CalculateCashbackAndCommissionResult.Failed.InsufficientBalance -> + Message.SetError(CommissionCalculatorRedux.State.Error.InsufficientBalance) + + is CalculateCashbackAndCommissionResult.Success -> Message.UpdateCommissionAndCashback( + inputAmount = result.inputAmount, + commission = result.commission, + cashback = result.cashback, + ) + } + } } \ No newline at end of file diff --git a/mvi-sample/domain/src/commonMain/kotlin/net/humans/kmm/mvi/sample/domain/CommissionCalculatorReducer.kt b/mvi-sample/domain/src/commonMain/kotlin/net/humans/kmm/mvi/sample/domain/CommissionCalculatorReducer.kt index 9a1a736..6bf8ec6 100644 --- a/mvi-sample/domain/src/commonMain/kotlin/net/humans/kmm/mvi/sample/domain/CommissionCalculatorReducer.kt +++ b/mvi-sample/domain/src/commonMain/kotlin/net/humans/kmm/mvi/sample/domain/CommissionCalculatorReducer.kt @@ -11,13 +11,15 @@ import net.humans.kmm.mvi.withEffect class CommissionCalculatorReducer : ComplexReducer { override fun invoke(state: State, msg: Message): Return = when (msg) { is Message.UpdateCommissionAndCashback -> state.copy( + inputAmount = msg.inputAmount, commission = msg.commission, cashback = msg.cashback, ).pure() - is Message.UpdateInput -> state.copy( + is Message.UpdateInput -> state withEffect Effect.CalculateCommissionAndCashback( + balance = state.balance, inputAmount = msg.inputAmount - ) withEffect Effect.CalculateCommissionAndCashback(inputAmount = msg.inputAmount) + ) is Message.UpdateState -> State( balance = msg.balance, @@ -25,5 +27,8 @@ class CommissionCalculatorReducer : ComplexReducer { commission = msg.commission, cashback = msg.cashback, ).pure() + + Message.ErrorHandled -> state.copy(error = null).pure() + is Message.SetError -> state.copy(error = msg.error).pure() } } \ No newline at end of file diff --git a/mvi-sample/domain/src/commonMain/kotlin/net/humans/kmm/mvi/sample/domain/CommissionCalculatorRedux.kt b/mvi-sample/domain/src/commonMain/kotlin/net/humans/kmm/mvi/sample/domain/CommissionCalculatorRedux.kt index 3a9dbc1..eb8d356 100644 --- a/mvi-sample/domain/src/commonMain/kotlin/net/humans/kmm/mvi/sample/domain/CommissionCalculatorRedux.kt +++ b/mvi-sample/domain/src/commonMain/kotlin/net/humans/kmm/mvi/sample/domain/CommissionCalculatorRedux.kt @@ -10,7 +10,13 @@ object CommissionCalculatorRedux { val inputAmount: MoneyAmount, val commission: MoneyAmount, val cashback: MoneyAmount, + val error: Error? = null, ) { + + sealed class Error { + object InsufficientBalance : Error() + } + companion object { val DEFAULT = State( balance = BigDecimal.ZERO.usd, @@ -34,15 +40,23 @@ object CommissionCalculatorRedux { ) : Message() data class UpdateCommissionAndCashback( + val inputAmount: MoneyAmount, val commission: MoneyAmount, val cashback: MoneyAmount, ) : Message() + + data class SetError( + val error: State.Error, + ) : Message() + + object ErrorHandled : Message() } sealed class Effect { object Initialize : Effect() data class CalculateCommissionAndCashback( + val balance: MoneyAmount, val inputAmount: MoneyAmount, ) : Effect() } diff --git a/mvi-sample/domain/src/commonMain/kotlin/net/humans/kmm/mvi/sample/domain/model/MoneyAmount.kt b/mvi-sample/domain/src/commonMain/kotlin/net/humans/kmm/mvi/sample/domain/model/MoneyAmount.kt index d4c513d..7a7d940 100644 --- a/mvi-sample/domain/src/commonMain/kotlin/net/humans/kmm/mvi/sample/domain/model/MoneyAmount.kt +++ b/mvi-sample/domain/src/commonMain/kotlin/net/humans/kmm/mvi/sample/domain/model/MoneyAmount.kt @@ -8,6 +8,13 @@ data class MoneyAmount( ) { operator fun times(value: Float): MoneyAmount = this.copy(amount = amount * BigDecimal.fromFloat(value)) + + operator fun compareTo(moneyAmount: MoneyAmount): Int { + check(this.currency == moneyAmount.currency) { + "Impossible to compare money amount. Non consistent currencies." + } + return this.amount.compareTo(moneyAmount.amount) + } } val BigDecimal.usd: MoneyAmount diff --git a/mvi-sample/domain/src/commonMain/kotlin/net/humans/kmm/mvi/sample/domain/usecase/CalculateCashbackAndCommissionUseCase.kt b/mvi-sample/domain/src/commonMain/kotlin/net/humans/kmm/mvi/sample/domain/usecase/CalculateCashbackAndCommissionUseCase.kt index 0a33ccb..8ea5d01 100644 --- a/mvi-sample/domain/src/commonMain/kotlin/net/humans/kmm/mvi/sample/domain/usecase/CalculateCashbackAndCommissionUseCase.kt +++ b/mvi-sample/domain/src/commonMain/kotlin/net/humans/kmm/mvi/sample/domain/usecase/CalculateCashbackAndCommissionUseCase.kt @@ -9,10 +9,18 @@ interface CalculateCashbackAndCommissionUseCase { } data class CalculateCashbackAndCommissionInput( + val balance: MoneyAmount, val inputAmount: MoneyAmount, ) -data class CalculateCashbackAndCommissionResult( - val cashback: MoneyAmount, - val commission: MoneyAmount, -) \ No newline at end of file +sealed class CalculateCashbackAndCommissionResult { + data class Success( + val inputAmount: MoneyAmount, + val cashback: MoneyAmount, + val commission: MoneyAmount, + ) : CalculateCashbackAndCommissionResult() + + sealed class Failed : CalculateCashbackAndCommissionResult() { + object InsufficientBalance : Failed() + } +} \ No newline at end of file diff --git a/mvi-sample/domain/src/commonMain/kotlin/net/humans/kmm/mvi/sample/domain/usecase/impl/RandomCalculateCashbackAndCommissionUseCase.kt b/mvi-sample/domain/src/commonMain/kotlin/net/humans/kmm/mvi/sample/domain/usecase/impl/RandomCalculateCashbackAndCommissionUseCase.kt index f05179f..9f3681f 100644 --- a/mvi-sample/domain/src/commonMain/kotlin/net/humans/kmm/mvi/sample/domain/usecase/impl/RandomCalculateCashbackAndCommissionUseCase.kt +++ b/mvi-sample/domain/src/commonMain/kotlin/net/humans/kmm/mvi/sample/domain/usecase/impl/RandomCalculateCashbackAndCommissionUseCase.kt @@ -10,10 +10,13 @@ internal class RandomCalculateCashbackAndCommissionUseCase( ) : CalculateCashbackAndCommissionUseCase { override suspend fun execute( input: CalculateCashbackAndCommissionInput - ): CalculateCashbackAndCommissionResult { - return CalculateCashbackAndCommissionResult( - cashback = input.inputAmount * commissionPercentage, - commission = input.inputAmount * cashbackPercentage, + ): CalculateCashbackAndCommissionResult = if (input.inputAmount > input.balance) { + CalculateCashbackAndCommissionResult.Failed.InsufficientBalance + } else { + CalculateCashbackAndCommissionResult.Success( + inputAmount = input.inputAmount, + cashback = input.inputAmount * cashbackPercentage, + commission = input.inputAmount * commissionPercentage, ) } diff --git a/mvi-sample/presentation/src/main/kotlin/net/humans/kmm/mvi/sample/CommissionCalculatorView.kt b/mvi-sample/presentation/src/main/kotlin/net/humans/kmm/mvi/sample/CommissionCalculatorView.kt index 08d76fe..ecb0b2c 100644 --- a/mvi-sample/presentation/src/main/kotlin/net/humans/kmm/mvi/sample/CommissionCalculatorView.kt +++ b/mvi-sample/presentation/src/main/kotlin/net/humans/kmm/mvi/sample/CommissionCalculatorView.kt @@ -9,6 +9,7 @@ import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue @@ -61,12 +62,20 @@ internal fun CommissionCalculatorView( onValueChange = { onValueChange(it.text) }, + isError = viewState.hasError(), visualTransformation = CurrencyAmountInputVisualTransformation(), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), textStyle = MaterialTheme.typography.displaySmall.copy( color = MaterialTheme.colorScheme.primary ) ) + viewState.error?.also { safeError -> + Text( + text = stringResource(id = safeError), + style = MaterialTheme.typography.displaySmall, + color = MaterialTheme.colorScheme.error, + ) + } } } @@ -77,7 +86,7 @@ private fun PreviewCommissionCalculatorView() { CommissionCalculatorView( viewState = CommissionCalculatorViewState( balance = "100", - inputAmount = TextFieldValue(text="50.00",selection = TextRange(5)), + inputAmount = TextFieldValue(text = "50.00", selection = TextRange(5)), commission = "1", cashback = "0.5", ), diff --git a/mvi-sample/presentation/src/main/kotlin/net/humans/kmm/mvi/sample/CommissionCalculatorViewModel.kt b/mvi-sample/presentation/src/main/kotlin/net/humans/kmm/mvi/sample/CommissionCalculatorViewModel.kt index a173dc9..48d7ab4 100644 --- a/mvi-sample/presentation/src/main/kotlin/net/humans/kmm/mvi/sample/CommissionCalculatorViewModel.kt +++ b/mvi-sample/presentation/src/main/kotlin/net/humans/kmm/mvi/sample/CommissionCalculatorViewModel.kt @@ -16,6 +16,7 @@ import net.humans.kmm.mvi.sample.domain.CommissionCalculatorRedux.Message import net.humans.kmm.mvi.sample.domain.CommissionCalculatorRedux.State import net.humans.kmm.mvi.sample.domain.model.usd import net.humans.kmm.mvi.withEffect +import net.humans.kmm.mvi.send internal class CommissionCalculatorViewModel( reducer: ComplexReducer = CommissionCalculatorReducer(), @@ -37,6 +38,7 @@ internal class CommissionCalculatorViewModel( fun inputAmountChange(input: String) { val amount = input.toFloatOrNull()?.let { it / INPUT_AMOUNT_DIVIDER } ?: DEFAULT_AMOUNT val moneyAmount = BigDecimal.fromFloat(amount).usd + engine send Message.ErrorHandled engine send Message.UpdateInput(moneyAmount) } diff --git a/mvi-sample/presentation/src/main/kotlin/net/humans/kmm/mvi/sample/CommissionCalculatorViewState.kt b/mvi-sample/presentation/src/main/kotlin/net/humans/kmm/mvi/sample/CommissionCalculatorViewState.kt index 43746fc..7990b81 100644 --- a/mvi-sample/presentation/src/main/kotlin/net/humans/kmm/mvi/sample/CommissionCalculatorViewState.kt +++ b/mvi-sample/presentation/src/main/kotlin/net/humans/kmm/mvi/sample/CommissionCalculatorViewState.kt @@ -1,5 +1,6 @@ package net.humans.kmm.mvi.sample +import androidx.annotation.StringRes import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue @@ -8,4 +9,7 @@ internal data class CommissionCalculatorViewState( val inputAmount: TextFieldValue = TextFieldValue(text = "", selection = TextRange(0)), val commission: String = "", val cashback: String = "", -) \ No newline at end of file + @StringRes val error: Int? = null, +) { + fun hasError(): Boolean = error != null +} \ No newline at end of file diff --git a/mvi-sample/presentation/src/main/kotlin/net/humans/kmm/mvi/sample/CommissionCalculatorViewStateConverter.kt b/mvi-sample/presentation/src/main/kotlin/net/humans/kmm/mvi/sample/CommissionCalculatorViewStateConverter.kt index f00d698..bd6456f 100644 --- a/mvi-sample/presentation/src/main/kotlin/net/humans/kmm/mvi/sample/CommissionCalculatorViewStateConverter.kt +++ b/mvi-sample/presentation/src/main/kotlin/net/humans/kmm/mvi/sample/CommissionCalculatorViewStateConverter.kt @@ -1,5 +1,6 @@ package net.humans.kmm.mvi.sample +import androidx.annotation.StringRes import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue import com.ionspin.kotlin.bignum.decimal.RoundingMode @@ -21,6 +22,7 @@ internal class CommissionCalculatorViewStateConverter : }, commission = state.commission.toPresentationString(), cashback = state.cashback.toPresentationString(), + error = state.error?.toPresentationError(), ) private fun MoneyAmount.toPresentationString(): String = @@ -31,4 +33,10 @@ internal class CommissionCalculatorViewStateConverter : private fun Currency.toPresentationString(): String = when (this) { Currency.USD -> "$" } + + @StringRes + private fun CommissionCalculatorRedux.State.Error.toPresentationError(): Int = when(this) { + CommissionCalculatorRedux.State.Error.InsufficientBalance -> + R.string.commission_calculator__error_insufficient_balance + } } diff --git a/mvi-sample/presentation/src/main/res/values/strings.xml b/mvi-sample/presentation/src/main/res/values/strings.xml index 6e22e0b..e8b13d3 100644 --- a/mvi-sample/presentation/src/main/res/values/strings.xml +++ b/mvi-sample/presentation/src/main/res/values/strings.xml @@ -1,3 +1,4 @@ Humans MVI + Insufficient balance \ No newline at end of file