diff --git a/bank-sdk/sdk/build.gradle.kts b/bank-sdk/sdk/build.gradle.kts
index 9cfc94ad7..b1173dbdc 100644
--- a/bank-sdk/sdk/build.gradle.kts
+++ b/bank-sdk/sdk/build.gradle.kts
@@ -135,6 +135,10 @@ dependencies {
implementation(libs.koin.android)
implementation(libs.koin.androidx.compose)
+ implementation(libs.orbitmvi.test)
+ implementation(libs.orbitmvi.compose)
+ implementation(libs.orbitmvi.viewmodel)
+
testImplementation(libs.junit)
testImplementation(libs.mockk)
testImplementation(libs.kotlinx.coroutines.test)
diff --git a/bank-sdk/sdk/detekt-baseline.xml b/bank-sdk/sdk/detekt-baseline.xml
index 92023d97f..b2bdaf5e9 100644
--- a/bank-sdk/sdk/detekt-baseline.xml
+++ b/bank-sdk/sdk/detekt-baseline.xml
@@ -137,7 +137,7 @@
NewLineAtEndOfFile:SkontoFragment.kt$net.gini.android.bank.sdk.capture.skonto.SkontoFragment.kt
NewLineAtEndOfFile:SkontoFragmentContract.kt$net.gini.android.bank.sdk.capture.skonto.SkontoFragmentContract.kt
NewLineAtEndOfFile:SkontoFragmentListener.kt$net.gini.android.bank.sdk.capture.skonto.SkontoFragmentListener.kt
- NewLineAtEndOfFile:SkontoFragmentViewModel.kt$net.gini.android.bank.sdk.capture.skonto.SkontoFragmentViewModel.kt
+ NewLineAtEndOfFile:SkontoFragmentViewModel.kt$net.gini.android.bank.sdk.capture.skonto.viewmodel.SkontoFragmentViewModel.kt
NewLineAtEndOfFile:SkontoInfoDialogColors.kt$net.gini.android.bank.sdk.capture.skonto.colors.section.SkontoInfoDialogColors.kt
NewLineAtEndOfFile:SkontoInvoiceScanSectionColors.kt$net.gini.android.bank.sdk.capture.skonto.colors.section.SkontoInvoiceScanSectionColors.kt
NewLineAtEndOfFile:SkontoNavigationBarBottomAdapter.kt$net.gini.android.bank.sdk.capture.skonto.SkontoNavigationBarBottomAdapter.kt
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/DigitalInvoiceSkontoFragment.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/DigitalInvoiceSkontoFragment.kt
index c330040c3..85852d8db 100644
--- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/DigitalInvoiceSkontoFragment.kt
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/DigitalInvoiceSkontoFragment.kt
@@ -2,7 +2,6 @@
package net.gini.android.bank.sdk.capture.digitalinvoice.skonto
-import android.annotation.SuppressLint
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.icu.util.Calendar
import android.os.Bundle
@@ -44,7 +43,6 @@ import androidx.compose.material3.SelectableDates
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.mutableStateOf
import androidx.compose.runtime.remember
@@ -55,7 +53,6 @@ import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
@@ -65,23 +62,26 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
+import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.fragment.app.setFragmentResult
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.findNavController
import androidx.navigation.fragment.navArgs
import net.gini.android.bank.sdk.GiniBank
import net.gini.android.bank.sdk.R
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.args.DigitalInvoiceSkontoResultArgs
import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.colors.DigitalInvoiceSkontoScreenColors
import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.colors.section.DigitalInvoiceSkontoFooterSectionColors
import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.colors.section.DigitalInvoiceSkontoInfoDialogColors
import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.colors.section.DigitalInvoiceSkontoInvoicePreviewSectionColors
import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.colors.section.DigitalInvoiceSkontoSectionColors
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.mapper.toErrorMessage
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.viewmodel.DigitalInvoiceSkontoViewModel
import net.gini.android.bank.sdk.capture.skonto.model.SkontoData
import net.gini.android.bank.sdk.capture.skonto.model.SkontoEdgeCase
import net.gini.android.bank.sdk.di.koin.giniBankViewModel
import net.gini.android.bank.sdk.util.disallowScreenshots
+import net.gini.android.bank.sdk.util.ui.keyboardAsState
import net.gini.android.capture.Amount
import net.gini.android.capture.GiniCapture
import net.gini.android.capture.internal.util.ActivityHelper
@@ -93,8 +93,11 @@ import net.gini.android.capture.ui.components.topbar.GiniTopBar
import net.gini.android.capture.ui.components.topbar.GiniTopBarColors
import net.gini.android.capture.ui.theme.GiniTheme
import net.gini.android.capture.ui.theme.modifier.tabletMaxWidth
+import net.gini.android.capture.util.compose.keyboardPadding
import net.gini.android.capture.view.InjectedViewAdapterInstance
import org.koin.core.parameter.parametersOf
+import org.orbitmvi.orbit.compose.collectAsState
+import org.orbitmvi.orbit.compose.collectSideEffect
import java.math.BigDecimal
import java.math.RoundingMode
import java.time.LocalDate
@@ -149,12 +152,9 @@ class DigitalInvoiceSkontoFragment : Fragment() {
isBottomNavigationBarEnabled = isBottomNavigationBarEnabled,
customBottomNavBarAdapter = customBottomNavBarAdapter,
navigateBack = {
- setFragmentResult(REQUEST_KEY, Bundle().apply {
- putParcelable(
- RESULT_KEY,
- viewModel.provideFragmentResult()
- )
- }
+ setFragmentResult(
+ REQUEST_KEY,
+ bundleOf(RESULT_KEY to it)
)
findNavController().popBackStack()
},
@@ -182,27 +182,11 @@ class DigitalInvoiceSkontoFragment : Fragment() {
}
}
-@Composable
-@SuppressLint("ComposableNaming")
-private fun DigitalInvoiceSkontoViewModel.collectSideEffect(
- action: (DigitalInvoiceSkontoSideEffect) -> Unit
-) {
-
- val lifecycleOwner = LocalLifecycleOwner.current
-
- LaunchedEffect(sideEffectFlow, lifecycleOwner) {
- lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
- sideEffectFlow.collect {
- action(it)
- }
- }
- }
-}
@Composable
private fun ScreenContent(
isBottomNavigationBarEnabled: Boolean,
- navigateBack: () -> Unit,
+ navigateBack: (DigitalInvoiceSkontoResultArgs) -> Unit,
navigateToHelpScreen: () -> Unit,
navigateToInvoiceScreen: (documentId: String, infoTextLines: List) -> Unit,
viewModel: DigitalInvoiceSkontoViewModel,
@@ -211,16 +195,24 @@ private fun ScreenContent(
screenColorScheme: DigitalInvoiceSkontoScreenColors = DigitalInvoiceSkontoScreenColors.colors(),
) {
- BackHandler { navigateBack() }
+ BackHandler { viewModel.onBackClicked() }
- val state by viewModel.stateFlow.collectAsState()
+ val state by viewModel.collectAsState()
+ val keyboardState by keyboardAsState()
+
+ LaunchedEffect(keyboardState) {
+ viewModel.onKeyboardStateChanged(keyboardState)
+ }
viewModel.collectSideEffect {
when (it) {
- is DigitalInvoiceSkontoSideEffect.OpenInvoiceScreen ->
+ is SkontoSideEffect.OpenInvoiceScreen ->
navigateToInvoiceScreen(it.documentId, it.infoTextLines)
- DigitalInvoiceSkontoSideEffect.OpenHelpScreen ->
+ is SkontoSideEffect.NavigateBack ->
+ navigateBack(it.args)
+
+ SkontoSideEffect.OpenHelpScreen ->
navigateToHelpScreen()
}
}
@@ -232,18 +224,18 @@ private fun ScreenContent(
onSkontoAmountChange = viewModel::onSkontoAmountFieldChanged,
onDueDateChanged = viewModel::onSkontoDueDateChanged,
isBottomNavigationBarEnabled = isBottomNavigationBarEnabled,
- onBackClicked = navigateBack,
+ onBackClicked = viewModel::onBackClicked,
onInfoBannerClicked = viewModel::onInfoBannerClicked,
onInfoDialogDismissed = viewModel::onInfoDialogDismissed,
onInvoiceClicked = viewModel::onInvoiceClicked,
customBottomNavBarAdapter = customBottomNavBarAdapter,
- onHelpClicked = viewModel::onHelpClicked
+ onHelpClicked = viewModel::onHelpClicked,
)
}
@Composable
private fun ScreenStateContent(
- state: DigitalInvoiceSkontoScreenState,
+ state: SkontoScreenState,
isBottomNavigationBarEnabled: Boolean,
onSkontoAmountChange: (BigDecimal) -> Unit,
onDueDateChanged: (LocalDate) -> Unit,
@@ -257,7 +249,7 @@ private fun ScreenStateContent(
screenColorScheme: DigitalInvoiceSkontoScreenColors = DigitalInvoiceSkontoScreenColors.colors()
) {
when (state) {
- is DigitalInvoiceSkontoScreenState.Ready -> ScreenReadyState(
+ is SkontoScreenState.Ready -> ScreenReadyState(
modifier = modifier,
state = state,
screenColorScheme = screenColorScheme,
@@ -277,7 +269,7 @@ private fun ScreenStateContent(
@Composable
private fun ScreenReadyState(
- state: DigitalInvoiceSkontoScreenState.Ready,
+ state: SkontoScreenState.Ready,
onBackClicked: () -> Unit,
onInvoiceClicked: () -> Unit,
onHelpClicked: () -> Unit,
@@ -292,6 +284,8 @@ private fun ScreenReadyState(
) {
val scrollState = rememberScrollState()
+ val keyboardPadding by keyboardPadding(108.dp, scrollState)
+
Scaffold(
modifier = modifier,
containerColor = screenColorScheme.backgroundColor,
@@ -316,7 +310,8 @@ private fun ScreenReadyState(
Column(
modifier = Modifier
.padding(it)
- .verticalScroll(scrollState),
+ .verticalScroll(scrollState)
+ .padding(bottom = keyboardPadding),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Column(
@@ -352,6 +347,7 @@ private fun ScreenReadyState(
onDueDateChanged = onDueDateChanged,
edgeCase = state.edgeCase,
onInfoBannerClicked = onInfoBannerClicked,
+ skontoAmountValidationError = state.skontoAmountValidationError,
)
}
}
@@ -534,9 +530,11 @@ private fun SkontoSection(
onInfoBannerClicked: () -> Unit,
edgeCase: SkontoEdgeCase?,
colors: DigitalInvoiceSkontoSectionColors,
+ skontoAmountValidationError: SkontoScreenState.Ready.SkontoAmountValidationError?,
modifier: Modifier = Modifier,
) {
val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy")
+ val resources = LocalContext.current.resources
var isDatePickerVisible by remember { mutableStateOf(false) }
Card(
@@ -651,6 +649,10 @@ private fun SkontoSection(
)
}
},
+ isError = skontoAmountValidationError != null,
+ supportingText = skontoAmountValidationError?.toErrorMessage(
+ resources = resources,
+ )
)
val dueDateOnClickSource = remember { MutableInteractionSource() }
@@ -891,7 +893,7 @@ private fun Float.formatAsDiscountPercentage(): String {
return "${value.toString().trimEnd('0').trimEnd('.')}%"
}
-private val previewState = DigitalInvoiceSkontoScreenState.Ready(
+private val previewState = SkontoScreenState.Ready(
isSkontoSectionActive = true,
paymentInDays = 14,
skontoPercentage = BigDecimal("3"),
@@ -901,4 +903,5 @@ private val previewState = DigitalInvoiceSkontoScreenState.Ready(
paymentMethod = SkontoData.SkontoPaymentMethod.PayPal,
edgeCase = SkontoEdgeCase.PayByCashOnly,
edgeCaseInfoDialogVisible = false,
+ skontoAmountValidationError = null
)
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/DigitalInvoiceSkontoScreenModule.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/DigitalInvoiceSkontoScreenModule.kt
index 1565686d0..b409fe926 100644
--- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/DigitalInvoiceSkontoScreenModule.kt
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/DigitalInvoiceSkontoScreenModule.kt
@@ -1,6 +1,15 @@
package net.gini.android.bank.sdk.capture.digitalinvoice.skonto
import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.args.DigitalInvoiceSkontoArgs
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.validation.DigitalInvoiceSkontoAmountValidator
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.viewmodel.DigitalInvoiceSkontoViewModel
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.viewmodel.SkontoScreenInitialStateFactory
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.viewmodel.intent.BackClickIntent
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.viewmodel.intent.InfoBannerInteractionIntent
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.viewmodel.intent.InvoiceClickIntent
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.viewmodel.intent.KeyboardStateChangeIntent
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.viewmodel.intent.SkontoAmountFieldChangeIntent
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.viewmodel.intent.SkontoDueDateChangeIntent
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
@@ -8,11 +17,41 @@ val digitalInvoiceSkontoScreenModule = module {
viewModel { (data: DigitalInvoiceSkontoArgs) ->
DigitalInvoiceSkontoViewModel(
args = data,
- getSkontoDiscountPercentageUseCase = get(),
- getSkontoEdgeCaseUseCase = get(),
- getSkontoRemainingDaysUseCase = get(),
+ skontoScreenInitialStateFactory = get(),
+ invoiceClickIntent = get(),
+ backClickIntent = get(),
+ infoBannerInteractionIntent = get(),
+ keyboardStateChangeIntent = get(),
+ skontoDueDateChangeIntent = get(),
+ skontoAmountFieldChangeIntent = get()
+ )
+ }
+ factory { DigitalInvoiceSkontoAmountValidator() }
+ factory {
+ SkontoScreenInitialStateFactory(
+ getSkontoEdgeCaseUseCase = get()
+ )
+ }
+ factory {
+ InvoiceClickIntent(
lastAnalyzedDocumentProvider = get(),
- skontoInvoicePreviewTextLinesFactory = get()
+ skontoInvoicePreviewTextLinesFactory = get(),
)
}
+ factory { BackClickIntent() }
+ factory { InfoBannerInteractionIntent() }
+ factory { KeyboardStateChangeIntent() }
+ factory {
+ SkontoDueDateChangeIntent(
+ getSkontoRemainingDaysUseCase = get(),
+ getSkontoEdgeCaseUseCase = get(),
+ )
+ }
+ factory {
+ SkontoAmountFieldChangeIntent(
+ digitalInvoiceSkontoAmountValidator = get(),
+ getSkontoDiscountPercentageUseCase = get(),
+ )
+ }
+
}
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/DigitalInvoiceSkontoViewModel.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/DigitalInvoiceSkontoViewModel.kt
deleted file mode 100644
index cb5d917f9..000000000
--- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/DigitalInvoiceSkontoViewModel.kt
+++ /dev/null
@@ -1,166 +0,0 @@
-package net.gini.android.bank.sdk.capture.digitalinvoice.skonto
-
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.launch
-import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.args.DigitalInvoiceSkontoArgs
-import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.args.DigitalInvoiceSkontoResultArgs
-import net.gini.android.bank.sdk.capture.skonto.factory.lines.SkontoInvoicePreviewTextLinesFactory
-import net.gini.android.bank.sdk.capture.skonto.model.SkontoData
-import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoDiscountPercentageUseCase
-import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoEdgeCaseUseCase
-import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoRemainingDaysUseCase
-import net.gini.android.capture.analysis.LastAnalyzedDocumentProvider
-import java.math.BigDecimal
-import java.time.LocalDate
-
-internal class DigitalInvoiceSkontoViewModel(
- args: DigitalInvoiceSkontoArgs,
- private val lastAnalyzedDocumentProvider: LastAnalyzedDocumentProvider,
- private val getSkontoDiscountPercentageUseCase: GetSkontoDiscountPercentageUseCase,
- private val getSkontoEdgeCaseUseCase: GetSkontoEdgeCaseUseCase,
- private val getSkontoRemainingDaysUseCase: GetSkontoRemainingDaysUseCase,
- private val skontoInvoicePreviewTextLinesFactory: SkontoInvoicePreviewTextLinesFactory,
-) : ViewModel() {
-
- val stateFlow: MutableStateFlow =
- MutableStateFlow(createInitalState(args.data, args.isSkontoSectionActive))
-
- val sideEffectFlow: MutableSharedFlow = MutableSharedFlow()
-
- internal fun provideFragmentResult(): DigitalInvoiceSkontoResultArgs {
- val currentState =
- stateFlow.value as? DigitalInvoiceSkontoScreenState.Ready
- ?: error("Can't extract result. State is not ready")
-
- return DigitalInvoiceSkontoResultArgs(
- skontoData = SkontoData(
- skontoAmountToPay = currentState.skontoAmount,
- skontoDueDate = currentState.discountDueDate,
- skontoPercentageDiscounted = currentState.skontoPercentage,
- skontoRemainingDays = currentState.paymentInDays,
- fullAmountToPay = currentState.fullAmount,
- skontoPaymentMethod = currentState.paymentMethod,
- ),
- isSkontoEnabled = currentState.isSkontoSectionActive,
- )
- }
-
- private fun createInitalState(
- data: SkontoData,
- isSkontoSectionActive: Boolean,
- ): DigitalInvoiceSkontoScreenState.Ready {
-
-
- val discount = data.skontoPercentageDiscounted
-
- val paymentMethod =
- data.skontoPaymentMethod ?: SkontoData.SkontoPaymentMethod.Unspecified
- val edgeCase = getSkontoEdgeCaseUseCase.execute(data.skontoDueDate, paymentMethod)
-
- return DigitalInvoiceSkontoScreenState.Ready(
- isSkontoSectionActive = isSkontoSectionActive,
- paymentInDays = data.skontoRemainingDays,
- skontoPercentage = discount,
- skontoAmount = data.skontoAmountToPay,
- discountDueDate = data.skontoDueDate,
- fullAmount = data.fullAmountToPay,
- paymentMethod = paymentMethod,
- edgeCase = edgeCase,
- edgeCaseInfoDialogVisible = edgeCase != null,
- )
- }
-
- fun onSkontoAmountFieldChanged(newValue: BigDecimal) = viewModelScope.launch {
- val currentState =
- stateFlow.value as? DigitalInvoiceSkontoScreenState.Ready ?: return@launch
-
- if (newValue > currentState.fullAmount.value) {
- stateFlow.emit(
- currentState.copy(skontoAmount = currentState.skontoAmount)
- )
- return@launch
- }
-
- val discount = getSkontoDiscountPercentageUseCase.execute(
- newValue,
- currentState.fullAmount.value
- )
-
- val newSkontoAmount = currentState.skontoAmount.copy(value = newValue)
-
- stateFlow.emit(
- currentState.copy(
- skontoAmount = newSkontoAmount,
- skontoPercentage = discount,
- )
- )
- }
-
- fun onSkontoDueDateChanged(newDate: LocalDate) = viewModelScope.launch {
- val currentState =
- stateFlow.value as? DigitalInvoiceSkontoScreenState.Ready ?: return@launch
- val newPayInDays = getSkontoRemainingDaysUseCase.execute(newDate)
- stateFlow.emit(
- currentState.copy(
- discountDueDate = newDate,
- paymentInDays = newPayInDays,
- edgeCase = getSkontoEdgeCaseUseCase.execute(
- dueDate = newDate,
- paymentMethod = currentState.paymentMethod
- )
- )
- )
- }
-
- fun onInfoBannerClicked() = viewModelScope.launch {
- val currentState =
- stateFlow.value as? DigitalInvoiceSkontoScreenState.Ready ?: return@launch
- stateFlow.emit(
- currentState.copy(
- edgeCaseInfoDialogVisible = true,
- )
- )
- }
-
- fun onInfoDialogDismissed() = viewModelScope.launch {
- val currentState =
- stateFlow.value as? DigitalInvoiceSkontoScreenState.Ready ?: return@launch
- stateFlow.emit(
- currentState.copy(
- edgeCaseInfoDialogVisible = false,
- )
- )
- }
-
- fun onInvoiceClicked() = viewModelScope.launch {
- val currentState =
- stateFlow.value as? DigitalInvoiceSkontoScreenState.Ready ?: return@launch
- val documentId = lastAnalyzedDocumentProvider.provide()?.giniApiDocumentId ?: return@launch
-
- val skontoData = SkontoData(
- skontoAmountToPay = currentState.skontoAmount,
- skontoDueDate = currentState.discountDueDate,
- skontoPercentageDiscounted = currentState.skontoPercentage,
- skontoRemainingDays = currentState.paymentInDays,
- fullAmountToPay = currentState.fullAmount,
- skontoPaymentMethod = currentState.paymentMethod,
- )
- val infoTextLines = skontoInvoicePreviewTextLinesFactory.create(
- skontoData
- )
-
- sideEffectFlow.emit(
- DigitalInvoiceSkontoSideEffect.OpenInvoiceScreen(
- documentId,
- infoTextLines
- )
- )
- }
-
- fun onHelpClicked() = viewModelScope.launch {
- sideEffectFlow.emit(DigitalInvoiceSkontoSideEffect.OpenHelpScreen)
- }
-}
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/DigitalInvoiceSkontoScreenState.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/SkontoScreenState.kt
similarity index 66%
rename from bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/DigitalInvoiceSkontoScreenState.kt
rename to bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/SkontoScreenState.kt
index 191168660..ae58cd1a0 100644
--- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/DigitalInvoiceSkontoScreenState.kt
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/SkontoScreenState.kt
@@ -6,25 +6,24 @@ import net.gini.android.capture.Amount
import java.math.BigDecimal
import java.time.LocalDate
-internal sealed class DigitalInvoiceSkontoScreenState {
+internal sealed interface SkontoScreenState {
data class Ready(
val isSkontoSectionActive: Boolean,
val paymentInDays: Int,
val skontoPercentage: BigDecimal,
val skontoAmount: Amount,
+ val skontoAmountValidationError: SkontoAmountValidationError?,
val fullAmount: Amount,
val discountDueDate: LocalDate,
val paymentMethod: SkontoData.SkontoPaymentMethod,
val edgeCase: SkontoEdgeCase?,
val edgeCaseInfoDialogVisible: Boolean,
- ) : DigitalInvoiceSkontoScreenState()
-}
-
-internal sealed interface DigitalInvoiceSkontoSideEffect {
- data class OpenInvoiceScreen(val documentId: String, val infoTextLines: List) :
- DigitalInvoiceSkontoSideEffect
+ ) : SkontoScreenState {
- object OpenHelpScreen : DigitalInvoiceSkontoSideEffect
+ sealed interface SkontoAmountValidationError {
+ object SkontoAmountMoreThanFullAmount : SkontoAmountValidationError
+ }
+ }
}
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/SkontoSideEffect.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/SkontoSideEffect.kt
new file mode 100644
index 000000000..bf604c3f2
--- /dev/null
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/SkontoSideEffect.kt
@@ -0,0 +1,12 @@
+package net.gini.android.bank.sdk.capture.digitalinvoice.skonto
+
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.args.DigitalInvoiceSkontoResultArgs
+
+internal sealed interface SkontoSideEffect {
+ data class OpenInvoiceScreen(val documentId: String, val infoTextLines: List) :
+ SkontoSideEffect
+
+ object OpenHelpScreen : SkontoSideEffect
+
+ data class NavigateBack(val args: DigitalInvoiceSkontoResultArgs) : SkontoSideEffect
+}
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/mapper/SkontoAmountValidationErrorMapper.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/mapper/SkontoAmountValidationErrorMapper.kt
new file mode 100644
index 000000000..14f268638
--- /dev/null
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/mapper/SkontoAmountValidationErrorMapper.kt
@@ -0,0 +1,14 @@
+package net.gini.android.bank.sdk.capture.digitalinvoice.skonto.mapper
+
+import android.content.res.Resources
+import net.gini.android.bank.sdk.R
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.SkontoScreenState
+
+internal fun SkontoScreenState.Ready.SkontoAmountValidationError.toErrorMessage(
+ resources: Resources,
+): String = when (this) {
+ is SkontoScreenState.Ready.SkontoAmountValidationError.SkontoAmountMoreThanFullAmount ->
+ resources.getString(
+ R.string.gbs_skonto_section_discount_field_amount_validation_error_skonto_amount_more_than_full_amount
+ )
+}
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/validation/DigitalInvoiceSkontoAmountValidator.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/validation/DigitalInvoiceSkontoAmountValidator.kt
new file mode 100644
index 000000000..2b7ab84d2
--- /dev/null
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/validation/DigitalInvoiceSkontoAmountValidator.kt
@@ -0,0 +1,15 @@
+package net.gini.android.bank.sdk.capture.digitalinvoice.skonto.validation
+
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.SkontoScreenState
+import java.math.BigDecimal
+
+internal class DigitalInvoiceSkontoAmountValidator {
+
+ operator fun invoke(newSkontoAmount: BigDecimal, fullAmount: BigDecimal)
+ : SkontoScreenState.Ready.SkontoAmountValidationError? = when {
+ newSkontoAmount > fullAmount ->
+ SkontoScreenState.Ready.SkontoAmountValidationError.SkontoAmountMoreThanFullAmount
+
+ else -> null
+ }
+}
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/viewmodel/DigitalInvoiceSkontoViewModel.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/viewmodel/DigitalInvoiceSkontoViewModel.kt
new file mode 100644
index 000000000..32b12b3ba
--- /dev/null
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/viewmodel/DigitalInvoiceSkontoViewModel.kt
@@ -0,0 +1,60 @@
+package net.gini.android.bank.sdk.capture.digitalinvoice.skonto.viewmodel
+
+import androidx.lifecycle.ViewModel
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.SkontoScreenState
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.SkontoSideEffect
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.args.DigitalInvoiceSkontoArgs
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.viewmodel.intent.BackClickIntent
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.viewmodel.intent.InfoBannerInteractionIntent
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.viewmodel.intent.InvoiceClickIntent
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.viewmodel.intent.KeyboardStateChangeIntent
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.viewmodel.intent.SkontoAmountFieldChangeIntent
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.viewmodel.intent.SkontoDueDateChangeIntent
+import org.orbitmvi.orbit.Container
+import org.orbitmvi.orbit.ContainerHost
+import org.orbitmvi.orbit.viewmodel.container
+import java.math.BigDecimal
+import java.time.LocalDate
+
+internal typealias SkontoContainerHost =
+ ContainerHost
+
+internal class DigitalInvoiceSkontoViewModel(
+ args: DigitalInvoiceSkontoArgs,
+ skontoScreenInitialStateFactory: SkontoScreenInitialStateFactory,
+ private val invoiceClickIntent: InvoiceClickIntent,
+ private val backClickIntent: BackClickIntent,
+ private val infoBannerInteractionIntent: InfoBannerInteractionIntent,
+ private val keyboardStateChangeIntent: KeyboardStateChangeIntent,
+ private val skontoDueDateChangeIntent: SkontoDueDateChangeIntent,
+ private val skontoAmountFieldChangeIntent: SkontoAmountFieldChangeIntent,
+) : ViewModel(), SkontoContainerHost {
+
+ override val container: Container =
+ container(skontoScreenInitialStateFactory.create(args.data, args.isSkontoSectionActive))
+
+ fun onSkontoAmountFieldChanged(newValue: BigDecimal) =
+ with(skontoAmountFieldChangeIntent) { run(newValue) }
+
+ fun onSkontoDueDateChanged(newDate: LocalDate) =
+ with(skontoDueDateChangeIntent) { run(newDate) }
+
+ fun onKeyboardStateChanged(isVisible: Boolean) =
+ with(keyboardStateChangeIntent) { run(isVisible) }
+
+ fun onInfoBannerClicked() =
+ with(infoBannerInteractionIntent) { runClick() }
+
+ fun onInfoDialogDismissed() =
+ with(infoBannerInteractionIntent) { runDismiss() }
+
+ fun onInvoiceClicked() =
+ with(invoiceClickIntent) { run() }
+
+ fun onBackClicked() =
+ with(backClickIntent) { run() }
+
+
+ fun onHelpClicked() =
+ intent { postSideEffect(SkontoSideEffect.OpenHelpScreen) }
+}
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/viewmodel/SkontoScreenInitialStateFactory.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/viewmodel/SkontoScreenInitialStateFactory.kt
new file mode 100644
index 000000000..1c20e8e23
--- /dev/null
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/viewmodel/SkontoScreenInitialStateFactory.kt
@@ -0,0 +1,30 @@
+package net.gini.android.bank.sdk.capture.digitalinvoice.skonto.viewmodel
+
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.SkontoScreenState
+import net.gini.android.bank.sdk.capture.skonto.model.SkontoData
+import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoEdgeCaseUseCase
+
+internal class SkontoScreenInitialStateFactory(
+ private val getSkontoEdgeCaseUseCase: GetSkontoEdgeCaseUseCase,
+) {
+
+ fun create(data: SkontoData, isSkontoSectionActive: Boolean): SkontoScreenState.Ready {
+ val discount = data.skontoPercentageDiscounted
+
+ val paymentMethod = data.skontoPaymentMethod ?: SkontoData.SkontoPaymentMethod.Unspecified
+ val edgeCase = getSkontoEdgeCaseUseCase.execute(data.skontoDueDate, paymentMethod)
+
+ return SkontoScreenState.Ready(
+ isSkontoSectionActive = isSkontoSectionActive,
+ paymentInDays = data.skontoRemainingDays,
+ skontoPercentage = discount,
+ skontoAmount = data.skontoAmountToPay,
+ discountDueDate = data.skontoDueDate,
+ fullAmount = data.fullAmountToPay,
+ paymentMethod = paymentMethod,
+ edgeCase = edgeCase,
+ edgeCaseInfoDialogVisible = edgeCase != null,
+ skontoAmountValidationError = null,
+ )
+ }
+}
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/viewmodel/intent/BackClickIntent.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/viewmodel/intent/BackClickIntent.kt
new file mode 100644
index 000000000..b4858ab59
--- /dev/null
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/viewmodel/intent/BackClickIntent.kt
@@ -0,0 +1,27 @@
+package net.gini.android.bank.sdk.capture.digitalinvoice.skonto.viewmodel.intent
+
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.SkontoScreenState
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.SkontoSideEffect
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.args.DigitalInvoiceSkontoResultArgs
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.viewmodel.SkontoContainerHost
+import net.gini.android.bank.sdk.capture.skonto.model.SkontoData
+
+internal class BackClickIntent {
+
+ fun SkontoContainerHost.run() = intent {
+ val state = state as? SkontoScreenState.Ready ?: return@intent
+ val args = DigitalInvoiceSkontoResultArgs(
+ skontoData = SkontoData(
+ skontoAmountToPay = state.skontoAmount,
+ skontoDueDate = state.discountDueDate,
+ skontoPercentageDiscounted = state.skontoPercentage,
+ skontoRemainingDays = state.paymentInDays,
+ fullAmountToPay = state.fullAmount,
+ skontoPaymentMethod = state.paymentMethod,
+ ),
+ isSkontoEnabled = state.isSkontoSectionActive,
+ )
+
+ postSideEffect(SkontoSideEffect.NavigateBack(args))
+ }
+}
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/viewmodel/intent/InfoBannerInteractionIntent.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/viewmodel/intent/InfoBannerInteractionIntent.kt
new file mode 100644
index 000000000..2d1ec94d8
--- /dev/null
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/viewmodel/intent/InfoBannerInteractionIntent.kt
@@ -0,0 +1,25 @@
+package net.gini.android.bank.sdk.capture.digitalinvoice.skonto.viewmodel.intent
+
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.SkontoScreenState
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.viewmodel.SkontoContainerHost
+
+internal class InfoBannerInteractionIntent {
+
+ fun SkontoContainerHost.runClick() = intent {
+ val state = state as? SkontoScreenState.Ready ?: return@intent
+ reduce {
+ state.copy(
+ edgeCaseInfoDialogVisible = true,
+ )
+ }
+ }
+
+ fun SkontoContainerHost.runDismiss() = intent {
+ val state = state as? SkontoScreenState.Ready ?: return@intent
+ reduce {
+ state.copy(
+ edgeCaseInfoDialogVisible = false,
+ )
+ }
+ }
+}
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/viewmodel/intent/InvoiceClickIntent.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/viewmodel/intent/InvoiceClickIntent.kt
new file mode 100644
index 000000000..e77d9aea0
--- /dev/null
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/viewmodel/intent/InvoiceClickIntent.kt
@@ -0,0 +1,34 @@
+package net.gini.android.bank.sdk.capture.digitalinvoice.skonto.viewmodel.intent
+
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.SkontoScreenState
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.SkontoSideEffect
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.viewmodel.SkontoContainerHost
+import net.gini.android.bank.sdk.capture.skonto.factory.lines.SkontoInvoicePreviewTextLinesFactory
+import net.gini.android.bank.sdk.capture.skonto.model.SkontoData
+import net.gini.android.capture.analysis.LastAnalyzedDocumentProvider
+
+internal class InvoiceClickIntent(
+ private val lastAnalyzedDocumentProvider: LastAnalyzedDocumentProvider,
+ private val skontoInvoicePreviewTextLinesFactory: SkontoInvoicePreviewTextLinesFactory
+) {
+
+ fun SkontoContainerHost.run() = intent {
+ val state = state as? SkontoScreenState.Ready ?: return@intent
+
+ val documentId = lastAnalyzedDocumentProvider.provide()?.giniApiDocumentId ?: return@intent
+
+ val skontoData = SkontoData(
+ skontoAmountToPay = state.skontoAmount,
+ skontoDueDate = state.discountDueDate,
+ skontoPercentageDiscounted = state.skontoPercentage,
+ skontoRemainingDays = state.paymentInDays,
+ fullAmountToPay = state.fullAmount,
+ skontoPaymentMethod = state.paymentMethod,
+ )
+ val infoTextLines = skontoInvoicePreviewTextLinesFactory.create(
+ skontoData
+ )
+
+ postSideEffect(SkontoSideEffect.OpenInvoiceScreen(documentId, infoTextLines))
+ }
+}
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/viewmodel/intent/KeyboardStateChangeIntent.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/viewmodel/intent/KeyboardStateChangeIntent.kt
new file mode 100644
index 000000000..c7032a599
--- /dev/null
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/viewmodel/intent/KeyboardStateChangeIntent.kt
@@ -0,0 +1,17 @@
+package net.gini.android.bank.sdk.capture.digitalinvoice.skonto.viewmodel.intent
+
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.SkontoScreenState
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.viewmodel.SkontoContainerHost
+
+internal class KeyboardStateChangeIntent {
+
+ fun SkontoContainerHost.run(visible: Boolean) = intent {
+ if (visible) return@intent
+ val state = state as? SkontoScreenState.Ready ?: return@intent
+ reduce {
+ state.copy(
+ skontoAmountValidationError = null
+ )
+ }
+ }
+}
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/viewmodel/intent/SkontoAmountFieldChangeIntent.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/viewmodel/intent/SkontoAmountFieldChangeIntent.kt
new file mode 100644
index 000000000..c014244d1
--- /dev/null
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/viewmodel/intent/SkontoAmountFieldChangeIntent.kt
@@ -0,0 +1,50 @@
+package net.gini.android.bank.sdk.capture.digitalinvoice.skonto.viewmodel.intent
+
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.SkontoScreenState
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.validation.DigitalInvoiceSkontoAmountValidator
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.viewmodel.SkontoContainerHost
+import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoDiscountPercentageUseCase
+import java.math.BigDecimal
+
+internal class SkontoAmountFieldChangeIntent(
+ private val digitalInvoiceSkontoAmountValidator: DigitalInvoiceSkontoAmountValidator,
+ private val getSkontoDiscountPercentageUseCase: GetSkontoDiscountPercentageUseCase,
+) {
+
+ fun SkontoContainerHost.run(newValue: BigDecimal) = intent {
+ val state = state as? SkontoScreenState.Ready ?: return@intent
+
+ if (newValue == state.skontoAmount.value) return@intent
+
+ val skontoAmountValidationError = digitalInvoiceSkontoAmountValidator(
+ newValue,
+ state.fullAmount.value
+ )
+
+ if (skontoAmountValidationError != null) {
+ reduce {
+ state.copy(
+ skontoAmount = state.skontoAmount,
+ skontoAmountValidationError = SkontoScreenState
+ .Ready.SkontoAmountValidationError.SkontoAmountMoreThanFullAmount
+ )
+ }
+ return@intent
+ }
+
+ val discount = getSkontoDiscountPercentageUseCase.execute(
+ newValue,
+ state.fullAmount.value
+ )
+
+ val newSkontoAmount = state.skontoAmount.copy(value = newValue)
+
+ reduce {
+ state.copy(
+ skontoAmount = newSkontoAmount,
+ skontoPercentage = discount,
+ skontoAmountValidationError = null,
+ )
+ }
+ }
+}
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/viewmodel/intent/SkontoDueDateChangeIntent.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/viewmodel/intent/SkontoDueDateChangeIntent.kt
new file mode 100644
index 000000000..9316c1982
--- /dev/null
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/viewmodel/intent/SkontoDueDateChangeIntent.kt
@@ -0,0 +1,28 @@
+package net.gini.android.bank.sdk.capture.digitalinvoice.skonto.viewmodel.intent
+
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.SkontoScreenState
+import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.viewmodel.SkontoContainerHost
+import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoEdgeCaseUseCase
+import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoRemainingDaysUseCase
+import java.time.LocalDate
+
+internal class SkontoDueDateChangeIntent(
+ private val getSkontoRemainingDaysUseCase: GetSkontoRemainingDaysUseCase,
+ private val getSkontoEdgeCaseUseCase: GetSkontoEdgeCaseUseCase,
+) {
+
+ fun SkontoContainerHost.run(newDate: LocalDate) = intent {
+ val state = state as? SkontoScreenState.Ready ?: return@intent
+ val newPayInDays = getSkontoRemainingDaysUseCase.execute(newDate)
+ reduce {
+ state.copy(
+ discountDueDate = newDate,
+ paymentInDays = newPayInDays,
+ edgeCase = getSkontoEdgeCaseUseCase.execute(
+ dueDate = newDate,
+ paymentMethod = state.paymentMethod
+ )
+ )
+ }
+ }
+}
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragment.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragment.kt
index 8194ca7c8..b0337bd15 100644
--- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragment.kt
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragment.kt
@@ -1,111 +1,27 @@
-@file:OptIn(ExperimentalMaterial3Api::class)
-
package net.gini.android.bank.sdk.capture.skonto
-import android.annotation.SuppressLint
-import android.content.res.Configuration.UI_MODE_NIGHT_YES
-import android.icu.util.Calendar
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
-import android.widget.FrameLayout
-import androidx.activity.compose.BackHandler
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.interaction.collectIsPressedAsState
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.IntrinsicSize
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material3.Button
-import androidx.compose.material3.ButtonDefaults
-import androidx.compose.material3.Card
-import androidx.compose.material3.CardDefaults
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.SelectableDates
-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.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.RectangleShape
-import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.ComposeView
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.ViewCompositionStrategy
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.pluralStringResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.viewinterop.AndroidView
-import androidx.compose.ui.window.Dialog
-import androidx.compose.ui.window.DialogProperties
import androidx.fragment.app.Fragment
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.findNavController
import androidx.navigation.fragment.navArgs
import net.gini.android.bank.sdk.GiniBank
import net.gini.android.bank.sdk.R
-import net.gini.android.bank.sdk.capture.skonto.colors.SkontoScreenColors
-import net.gini.android.bank.sdk.capture.skonto.colors.section.SkontoFooterSectionColors
-import net.gini.android.bank.sdk.capture.skonto.colors.section.SkontoInfoDialogColors
-import net.gini.android.bank.sdk.capture.skonto.colors.section.SkontoInvoicePreviewSectionColors
-import net.gini.android.bank.sdk.capture.skonto.colors.section.SkontoSectionColors
-import net.gini.android.bank.sdk.capture.skonto.colors.section.WithoutSkontoSectionColors
-import net.gini.android.bank.sdk.capture.skonto.formatter.SkontoDiscountPercentageFormatter
-import net.gini.android.bank.sdk.capture.skonto.model.SkontoData
-import net.gini.android.bank.sdk.capture.skonto.model.SkontoEdgeCase
-import net.gini.android.bank.sdk.capture.util.currencyFormatterWithoutSymbol
+import net.gini.android.bank.sdk.capture.skonto.formatter.AmountFormatter
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.SkontoFragmentViewModel
import net.gini.android.bank.sdk.di.getGiniBankKoin
-import net.gini.android.bank.sdk.transactiondocs.ui.dialog.attachdoc.AttachDocumentToTransactionDialog
import net.gini.android.bank.sdk.util.disallowScreenshots
-import net.gini.android.capture.Amount
import net.gini.android.capture.GiniCapture
import net.gini.android.capture.internal.util.ActivityHelper.forcePortraitOrientationOnPhones
import net.gini.android.capture.internal.util.CancelListener
-import net.gini.android.capture.ui.components.button.filled.GiniButton
-import net.gini.android.capture.ui.components.picker.date.GiniDatePickerDialog
-import net.gini.android.capture.ui.components.switcher.GiniSwitch
-import net.gini.android.capture.ui.components.textinput.GiniTextInput
-import net.gini.android.capture.ui.components.textinput.amount.GiniAmountTextInput
-import net.gini.android.capture.ui.components.tooltip.GiniTooltipBox
-import net.gini.android.capture.ui.components.topbar.GiniTopBar
-import net.gini.android.capture.ui.components.topbar.GiniTopBarColors
import net.gini.android.capture.ui.theme.GiniTheme
-import net.gini.android.capture.ui.theme.modifier.tabletMaxWidth
-import net.gini.android.capture.ui.theme.typography.bold
import net.gini.android.capture.view.InjectedViewAdapterInstance
import org.koin.core.parameter.parametersOf
-import java.math.BigDecimal
-import java.time.LocalDate
-import java.time.format.DateTimeFormatter
class SkontoFragment : Fragment() {
@@ -114,6 +30,7 @@ class SkontoFragment : Fragment() {
private val viewModel: SkontoFragmentViewModel by getGiniBankKoin().inject {
parametersOf(args.data)
}
+ private val amountFormatter : AmountFormatter by getGiniBankKoin().inject()
lateinit var cancelListener: CancelListener
@@ -154,7 +71,7 @@ class SkontoFragment : Fragment() {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
GiniTheme {
- ScreenContent(
+ SkontoScreenContent(
viewModel = viewModel,
isBottomNavigationBarEnabled = isBottomNavigationBarEnabled,
customBottomNavBarAdapter = customBottomNavBarAdapter,
@@ -177,949 +94,10 @@ class SkontoFragment : Fragment() {
navigateToHelp = {
findNavController().navigate(SkontoFragmentDirections.toSkontoHelpFragment())
},
+ amountFormatter = amountFormatter,
)
}
}
}
}
}
-
-@Composable
-@SuppressLint("ComposableNaming")
-private fun SkontoFragmentViewModel.collectSideEffect(
- action: (SkontoFragmentContract.SideEffect) -> Unit
-) {
-
- val lifecycleOwner = LocalLifecycleOwner.current
-
- LaunchedEffect(sideEffectFlow, lifecycleOwner) {
- lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
- sideEffectFlow.collect {
- action(it)
- }
- }
- }
-}
-
-@Composable
-private fun ScreenContent(
- navigateBack: () -> Unit,
- navigateToHelp: () -> Unit,
- viewModel: SkontoFragmentViewModel,
- modifier: Modifier = Modifier,
- screenColorScheme: SkontoScreenColors = SkontoScreenColors.colors(),
- isBottomNavigationBarEnabled: Boolean,
- customBottomNavBarAdapter: InjectedViewAdapterInstance?,
- navigateToInvoiceScreen: (documentId: String, infoTextLines: List) -> Unit,
-) {
-
- BackHandler { navigateBack() }
-
- val state by viewModel.stateFlow.collectAsState()
-
- viewModel.collectSideEffect {
- when (it) {
- is SkontoFragmentContract.SideEffect.OpenInvoiceScreen ->
- navigateToInvoiceScreen(it.documentId, it.infoTextLines)
- }
- }
-
- ScreenStateContent(
- modifier = modifier,
- state = state,
- screenColorScheme = screenColorScheme,
- onDiscountSectionActiveChange = viewModel::onSkontoActiveChanged,
- onSkontoAmountChange = viewModel::onSkontoAmountFieldChanged,
- onDueDateChanged = viewModel::onSkontoDueDateChanged,
- onFullAmountChange = viewModel::onFullAmountFieldChanged,
- isBottomNavigationBarEnabled = isBottomNavigationBarEnabled,
- onBackClicked = navigateBack,
- onHelpClicked = navigateToHelp,
- customBottomNavBarAdapter = customBottomNavBarAdapter,
- onProceedClicked = viewModel::onProceedClicked,
- onInfoBannerClicked = viewModel::onInfoBannerClicked,
- onInfoDialogDismissed = viewModel::onInfoDialogDismissed,
- onInvoiceClicked = viewModel::onInvoiceClicked,
- onConfirmAttachTransactionDocClicked = viewModel::onConfirmAttachTransactionDocClicked,
- onCancelAttachTransactionDocClicked = viewModel::onCancelAttachTransactionDocClicked,
- )
-}
-
-@Composable
-private fun ScreenStateContent(
- state: SkontoFragmentContract.State,
- onDiscountSectionActiveChange: (Boolean) -> Unit,
- onSkontoAmountChange: (BigDecimal) -> Unit,
- onFullAmountChange: (BigDecimal) -> Unit,
- onDueDateChanged: (LocalDate) -> Unit,
- onBackClicked: () -> Unit,
- onHelpClicked: () -> Unit,
- onProceedClicked: () -> Unit,
- isBottomNavigationBarEnabled: Boolean,
- customBottomNavBarAdapter: InjectedViewAdapterInstance?,
- onInfoBannerClicked: () -> Unit,
- onInfoDialogDismissed: () -> Unit,
- onInvoiceClicked: () -> Unit,
- onConfirmAttachTransactionDocClicked: (alwaysAttach: Boolean) -> Unit,
- onCancelAttachTransactionDocClicked: () -> Unit,
- modifier: Modifier = Modifier,
- screenColorScheme: SkontoScreenColors = SkontoScreenColors.colors()
-) {
- when (state) {
- is SkontoFragmentContract.State.Ready -> ScreenReadyState(
- modifier = modifier,
- state = state,
- screenColorScheme = screenColorScheme,
- onDiscountSectionActiveChange = onDiscountSectionActiveChange,
- onDiscountAmountChange = onSkontoAmountChange,
- onDueDateChanged = onDueDateChanged,
- onFullAmountChange = onFullAmountChange,
- onBackClicked = onBackClicked,
- onHelpClicked = onHelpClicked,
- isBottomNavigationBarEnabled = isBottomNavigationBarEnabled,
- customBottomNavBarAdapter = customBottomNavBarAdapter,
- onProceedClicked = onProceedClicked,
- onInfoBannerClicked = onInfoBannerClicked,
- onInfoDialogDismissed = onInfoDialogDismissed,
- onInvoiceClicked = onInvoiceClicked,
- onConfirmAttachTransactionDocClicked = onConfirmAttachTransactionDocClicked,
- onCancelAttachTransactionDocClicked = onCancelAttachTransactionDocClicked,
- )
- }
-
-}
-
-@Composable
-private fun ScreenReadyState(
- onConfirmAttachTransactionDocClicked: (alwaysAttach: Boolean) -> Unit,
- onCancelAttachTransactionDocClicked: () -> Unit,
- onBackClicked: () -> Unit,
- onHelpClicked: () -> Unit,
- onProceedClicked: () -> Unit,
- onInvoiceClicked: () -> Unit,
- state: SkontoFragmentContract.State.Ready,
- onDiscountSectionActiveChange: (Boolean) -> Unit,
- onDiscountAmountChange: (BigDecimal) -> Unit,
- onDueDateChanged: (LocalDate) -> Unit,
- onFullAmountChange: (BigDecimal) -> Unit,
- isBottomNavigationBarEnabled: Boolean,
- customBottomNavBarAdapter: InjectedViewAdapterInstance?,
- modifier: Modifier = Modifier,
- onInfoBannerClicked: () -> Unit,
- onInfoDialogDismissed: () -> Unit,
- discountPercentageFormatter: SkontoDiscountPercentageFormatter = SkontoDiscountPercentageFormatter(),
- screenColorScheme: SkontoScreenColors = SkontoScreenColors.colors(),
-) {
-
- val scrollState = rememberScrollState()
- Scaffold(modifier = modifier,
- containerColor = screenColorScheme.backgroundColor,
- topBar = {
- TopAppBar(
- isBottomNavigationBarEnabled = isBottomNavigationBarEnabled,
- colors = screenColorScheme.topAppBarColors,
- onBackClicked = onBackClicked,
- onHelpClicked = onHelpClicked
- )
- },
- bottomBar = {
- FooterSection(
- colors = screenColorScheme.footerSectionColors,
- discountValue = state.skontoPercentage,
- totalAmount = state.totalAmount,
- isBottomNavigationBarEnabled = isBottomNavigationBarEnabled,
- onBackClicked = onBackClicked,
- onHelpClicked = onHelpClicked,
- customBottomNavBarAdapter = customBottomNavBarAdapter,
- onProceedClicked = onProceedClicked,
- isSkontoSectionActive = state.isSkontoSectionActive,
- savedAmount = state.savedAmount,
- discountPercentageFormatter = discountPercentageFormatter,
- )
- }) {
- Column(
- modifier = Modifier
- .padding(it)
- .verticalScroll(scrollState),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- Column(
- modifier = Modifier
- .fillMaxWidth(),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.spacedBy(16.dp),
- ) {
- val invoicePreviewPaddingTop =
- if (LocalContext.current.resources.getBoolean(net.gini.android.capture.R.bool.gc_is_tablet)) {
- 64.dp
- } else {
- 8.dp
- }
- InvoicePreviewSection(
- modifier = Modifier
- .padding(top = invoicePreviewPaddingTop)
- .tabletMaxWidth(),
- colorScheme = screenColorScheme.invoiceScanSectionColors,
- onClick = onInvoiceClicked,
- )
- SkontoSection(
- modifier = Modifier
- .padding(top = 8.dp)
- .tabletMaxWidth(),
- colors = screenColorScheme.skontoSectionColors,
- amount = state.skontoAmount,
- dueDate = state.discountDueDate,
- infoPaymentInDays = state.paymentInDays,
- infoDiscountValue = state.skontoPercentage,
- onActiveChange = onDiscountSectionActiveChange,
- isActive = state.isSkontoSectionActive,
- onSkontoAmountChange = onDiscountAmountChange,
- onDueDateChanged = onDueDateChanged,
- edgeCase = state.skontoEdgeCase,
- onInfoBannerClicked = onInfoBannerClicked,
- discountPercentageFormatter = discountPercentageFormatter,
- )
- WithoutSkontoSection(
- modifier = Modifier.tabletMaxWidth(),
- colors = screenColorScheme.withoutSkontoSectionColors,
- isActive = !state.isSkontoSectionActive,
- amount = state.fullAmount,
- onFullAmountChange = onFullAmountChange,
- )
- }
- }
-
- if (state.edgeCaseInfoDialogVisible) {
- val text = when (state.skontoEdgeCase) {
- SkontoEdgeCase.PayByCashToday,
- SkontoEdgeCase.PayByCashOnly ->
- stringResource(id = R.string.gbs_skonto_section_info_dialog_pay_cash_message)
-
- SkontoEdgeCase.SkontoExpired ->
- stringResource(
- id = R.string.gbs_skonto_section_info_dialog_date_expired_message,
- discountPercentageFormatter.format(state.skontoPercentage.toFloat())
- )
-
- SkontoEdgeCase.SkontoLastDay ->
- stringResource(
- id = R.string.gbs_skonto_section_info_dialog_pay_today_message,
- )
-
- null -> ""
- }
- InfoDialog(
- text = text,
- colors = screenColorScheme.infoDialogColors,
- onDismissRequest = onInfoDialogDismissed
- )
- }
-
- if (state.transactionDialogVisible) {
- AttachDocumentToTransactionDialog(
- onDismiss = onCancelAttachTransactionDocClicked,
- onConfirm = onConfirmAttachTransactionDocClicked
- )
- }
- }
-}
-
-@Composable
-private fun TopAppBar(
- onBackClicked: () -> Unit,
- onHelpClicked: () -> Unit,
- modifier: Modifier = Modifier,
- isBottomNavigationBarEnabled: Boolean,
- colors: GiniTopBarColors,
-) {
- GiniTopBar(
- modifier = modifier,
- colors = colors,
- title = stringResource(id = R.string.gbs_skonto_screen_title),
- navigationIcon = {
- AnimatedVisibility(visible = !isBottomNavigationBarEnabled) {
- NavigationActionBack(
- modifier = Modifier.padding(start = 16.dp, end = 32.dp),
- onClick = onBackClicked
- )
- }
- },
- actions = {
- AnimatedVisibility(visible = !isBottomNavigationBarEnabled) {
- NavigationActionHelp(
- modifier = Modifier.padding(start = 20.dp, end = 12.dp),
- onClick = onHelpClicked
- )
- }
- })
-}
-
-@Composable
-private fun NavigationActionBack(
- onClick: () -> Unit,
- modifier: Modifier = Modifier,
-) {
- GiniTooltipBox(
- tooltipText = stringResource(
- id = R.string.gbs_skonto_screen_content_description_back
- )
- ) {
- IconButton(
- modifier = modifier
- .width(24.dp)
- .height(24.dp),
- onClick = onClick
- ) {
- Icon(
- painter = painterResource(id = net.gini.android.capture.R.drawable.gc_action_bar_back),
- contentDescription = stringResource(
- id = R.string.gbs_skonto_screen_content_description_back
- ),
- )
- }
- }
-}
-
-@Composable
-private fun NavigationActionHelp(
- onClick: () -> Unit,
- modifier: Modifier = Modifier,
-) {
- GiniTooltipBox(
- tooltipText = stringResource(
- id = R.string.gbs_skonto_screen_content_description_help
- )
- ) {
- IconButton(
- modifier = modifier
- .width(24.dp)
- .height(24.dp),
- onClick = onClick
- ) {
- Icon(
- painter = painterResource(R.drawable.gbs_help_question_icon),
- contentDescription = stringResource(
- id = R.string.gbs_skonto_screen_content_description_help
- ),
- )
- }
- }
-}
-
-@Composable
-private fun InvoicePreviewSection(
- modifier: Modifier = Modifier,
- colorScheme: SkontoInvoicePreviewSectionColors,
- onClick: () -> Unit,
-) {
- Card(
- modifier = modifier
- .fillMaxWidth()
- .clickable(onClick = onClick),
- shape = RectangleShape,
- colors = CardDefaults.cardColors(containerColor = colorScheme.cardBackgroundColor)
- ) {
- Row(
- modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically
- ) {
- Box(
- contentAlignment = Alignment.Center,
- modifier = Modifier
- .background(colorScheme.iconBackgroundColor, shape = RoundedCornerShape(4.dp))
- ) {
- Icon(
- modifier = Modifier
- .size(40.dp)
- .padding(8.dp),
- painter = painterResource(id = R.drawable.gbs_icon_document),
- contentDescription = null,
- tint = colorScheme.iconTint,
- )
- }
-
- Column(
- modifier = Modifier
- .padding(horizontal = 16.dp)
- .fillMaxWidth()
- .weight(0.1f)
- ) {
- Text(
- text = stringResource(id = R.string.gbs_skonto_section_invoice_preview_title),
- style = GiniTheme.typography.subtitle1,
- color = colorScheme.titleTextColor
- )
- Text(
- text = stringResource(id = R.string.gbs_skonto_invoice_section_preview_subtitle),
- style = GiniTheme.typography.body2,
- color = colorScheme.subtitleTextColor
- )
- }
-
- Icon(
- painter = painterResource(id = R.drawable.gbs_arrow_right),
- contentDescription = null,
- tint = colorScheme.arrowTint
- )
- }
-
- }
-}
-
-@Composable
-private fun SkontoSection(
- colors: SkontoSectionColors,
- amount: Amount,
- dueDate: LocalDate,
- infoPaymentInDays: Int,
- infoDiscountValue: BigDecimal,
- onActiveChange: (Boolean) -> Unit,
- onSkontoAmountChange: (BigDecimal) -> Unit,
- onDueDateChanged: (LocalDate) -> Unit,
- onInfoBannerClicked: () -> Unit,
- edgeCase: SkontoEdgeCase?,
- modifier: Modifier = Modifier,
- isActive: Boolean,
- discountPercentageFormatter: SkontoDiscountPercentageFormatter = SkontoDiscountPercentageFormatter()
-) {
- val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy")
-
- var isDatePickerVisible by remember { mutableStateOf(false) }
- Card(
- modifier = modifier,
- shape = RectangleShape,
- colors = CardDefaults.cardColors(containerColor = colors.cardBackgroundColor)
- ) {
- Column(
- modifier = Modifier.padding(16.dp)
- ) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.Start,
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Text(
- text = stringResource(id = R.string.gbs_skonto_section_discount_title),
- style = GiniTheme.typography.subtitle1,
- color = colors.titleTextColor,
- )
- Box {
- androidx.compose.animation.AnimatedVisibility(visible = isActive) {
- Text(
- text = stringResource(id = R.string.gbs_skonto_section_discount_hint_label_enabled),
- style = GiniTheme.typography.subtitle2,
- color = colors.enabledHintTextColor,
- )
- }
- }
-
- Spacer(Modifier.weight(1f))
-
-
- GiniSwitch(
- checked = isActive,
- onCheckedChange = onActiveChange,
- )
- }
- val animatedDiscountAmount by animateFloatAsState(
- targetValue = infoDiscountValue.toFloat(),
- label = "discountAmount"
- )
-
- val remainingDaysText =
- if (infoPaymentInDays != 0) {
- pluralStringResource(
- id = R.plurals.days,
- count = infoPaymentInDays,
- infoPaymentInDays.toString()
- )
- } else {
- stringResource(id = R.string.days_zero)
- }
-
- val infoBannerText = when (edgeCase) {
- SkontoEdgeCase.PayByCashOnly ->
- stringResource(
- id = R.string.gbs_skonto_section_discount_info_banner_pay_cash_message,
- discountPercentageFormatter.format(animatedDiscountAmount),
- remainingDaysText
- )
-
- SkontoEdgeCase.PayByCashToday ->
- stringResource(
- id = R.string.gbs_skonto_section_discount_info_banner_pay_cash_today_message,
- discountPercentageFormatter.format(animatedDiscountAmount)
- )
-
- SkontoEdgeCase.SkontoExpired ->
- stringResource(
- id = R.string.gbs_skonto_section_discount_info_banner_date_expired_message,
- discountPercentageFormatter.format(animatedDiscountAmount)
- )
-
- SkontoEdgeCase.SkontoLastDay ->
- stringResource(
- id = R.string.gbs_skonto_section_discount_info_banner_pay_today_message,
- discountPercentageFormatter.format(animatedDiscountAmount)
- )
-
- else -> stringResource(
- id = R.string.gbs_skonto_section_discount_info_banner_normal_message,
- remainingDaysText,
- discountPercentageFormatter.format(animatedDiscountAmount)
- )
- }
-
- InfoBanner(
- text = infoBannerText,
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 6.dp),
- colors = when (edgeCase) {
- SkontoEdgeCase.SkontoLastDay,
- SkontoEdgeCase.PayByCashToday,
- SkontoEdgeCase.PayByCashOnly -> colors.warningInfoBannerColors
-
- SkontoEdgeCase.SkontoExpired -> colors.errorInfoBannerColors
- else -> colors.successInfoBannerColors
- },
- onClicked = onInfoBannerClicked,
- clickable = edgeCase != null,
- )
- GiniAmountTextInput(
- amount = amount.value,
- currencyCode = amount.currency.name,
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 16.dp),
- enabled = isActive,
- colors = colors.amountFieldColors,
- onValueChange = { onSkontoAmountChange(it) },
- label = stringResource(id = R.string.gbs_skonto_section_discount_field_amount_hint),
- trailingContent = {
- AnimatedVisibility(visible = isActive) {
- Text(
- text = amount.currency.name,
- style = GiniTheme.typography.subtitle1,
- )
- }
- },
- )
-
- val dueDateOnClickSource = remember { MutableInteractionSource() }
- val pressed by dueDateOnClickSource.collectIsPressedAsState()
-
- LaunchedEffect(key1 = pressed) {
- if (pressed) {
- isDatePickerVisible = true
- }
- }
-
- GiniTextInput(
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 16.dp)
- .focusable(false),
- enabled = isActive,
- interactionSource = dueDateOnClickSource,
- readOnly = true,
- colors = colors.dueDateTextFieldColor,
- onValueChange = { /* Ignored */ },
- text = dueDate.format(dateFormatter),
- label = stringResource(id = R.string.gbs_skonto_section_discount_field_due_date_hint),
- trailingContent = {
- androidx.compose.animation.AnimatedVisibility(visible = isActive) {
- Icon(
- painter = painterResource(id = R.drawable.gbs_icon_calendar),
- contentDescription = null,
- )
- }
- },
- )
- }
- }
-
- if (isDatePickerVisible) {
- GiniDatePickerDialog(
- onDismissRequest = { isDatePickerVisible = false },
- onSaved = {
- isDatePickerVisible = false
- onDueDateChanged(it)
- },
- date = dueDate,
- selectableDates = getSkontoSelectableDates()
- )
- }
-}
-
-private fun getSkontoSelectableDates() = object : SelectableDates {
-
- val minDateCalendar = Calendar.getInstance().apply {
- set(Calendar.MILLISECONDS_IN_DAY, 0)
- }
-
- val maxDateCalendar = Calendar.getInstance().apply {
- add(Calendar.MONTH, 6)
- }
-
- val minTime = minDateCalendar.timeInMillis
- val maxTime = maxDateCalendar.timeInMillis
-
- override fun isSelectableDate(utcTimeMillis: Long): Boolean {
- return (minTime..maxTime).contains(utcTimeMillis)
- }
-
- override fun isSelectableYear(year: Int): Boolean {
- return (minDateCalendar.get(Calendar.YEAR)..maxDateCalendar.get(Calendar.YEAR))
- .contains(year)
- }
-}
-
-@Composable
-private fun InfoBanner(
- colors: SkontoSectionColors.InfoBannerColors,
- text: String,
- clickable: Boolean,
- onClicked: () -> Unit,
- modifier: Modifier = Modifier,
- icon: Painter = painterResource(id = R.drawable.gbs_icon_important_info),
-) {
- Row(
- modifier = modifier
- .background(
- color = colors.backgroundColor, RoundedCornerShape(8.dp)
- )
- .clickable(onClick = onClicked, enabled = clickable),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Icon(
- modifier = Modifier.padding(8.dp),
- painter = icon,
- contentDescription = null,
- tint = colors.iconTint,
- )
-
- Text(
- modifier = Modifier.padding(top = 12.dp, bottom = 12.dp, end = 16.dp),
- text = text,
- style = GiniTheme.typography.subtitle2,
- color = colors.textColor,
- )
- }
-}
-
-@Composable
-private fun InfoDialog(
- text: String,
- colors: SkontoInfoDialogColors,
- onDismissRequest: () -> Unit,
- modifier: Modifier = Modifier,
-) {
- Dialog(
- properties = DialogProperties(),
- onDismissRequest = onDismissRequest
- ) {
- Card(
- modifier = modifier.fillMaxWidth(),
- shape = RoundedCornerShape(28.dp),
- colors = CardDefaults.cardColors(
- containerColor = colors.cardBackgroundColor
- )
- ) {
- Text(
- modifier = Modifier.padding(top = 24.dp, start = 16.dp, end = 16.dp),
- text = text,
- style = GiniTheme.typography.caption1
- )
- Button(
- modifier = Modifier
- .padding(16.dp)
- .align(Alignment.End),
- onClick = onDismissRequest,
- shape = RoundedCornerShape(4.dp),
- colors = ButtonDefaults.textButtonColors(
- contentColor = colors.buttonTextColor,
- ),
- ) {
- Text(
- modifier = Modifier,
- text = stringResource(id = R.string.gbs_skonto_section_info_dialog_ok_button_text),
- style = GiniTheme.typography.button
- )
- }
- }
- }
-}
-
-@Composable
-private fun WithoutSkontoSection(
- colors: WithoutSkontoSectionColors,
- amount: Amount,
- modifier: Modifier = Modifier,
- onFullAmountChange: (BigDecimal) -> Unit,
- isActive: Boolean,
-) {
- Card(
- modifier = modifier,
- shape = RectangleShape,
- colors = CardDefaults.cardColors(containerColor = colors.cardBackgroundColor)
- ) {
- Column(
- modifier = Modifier.padding(16.dp)
- ) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.Start,
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Text(
- text = stringResource(id = R.string.gbs_skonto_section_without_discount_title),
- style = GiniTheme.typography.subtitle1,
- color = colors.titleTextColor,
- )
- AnimatedVisibility(visible = isActive) {
- Text(
- text = stringResource(id = R.string.gbs_skonto_section_discount_hint_label_enabled),
- modifier = Modifier.weight(0.1f),
- style = GiniTheme.typography.subtitle2,
- color = colors.enabledHintTextColor,
- )
- }
- }
- GiniAmountTextInput(
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 16.dp),
- enabled = isActive,
- colors = colors.amountFieldColors,
- amount = amount.value,
- currencyCode = amount.currency.name,
- onValueChange = onFullAmountChange,
- label = stringResource(id = R.string.gbs_skonto_section_without_discount_field_amount_hint),
- trailingContent = {
- AnimatedVisibility(visible = isActive) {
- Text(
- text = amount.currency.name,
- style = GiniTheme.typography.subtitle1,
- )
- }
- },
- )
- }
- }
-}
-
-@Composable
-private fun FooterSection(
- totalAmount: Amount,
- savedAmount: Amount,
- discountValue: BigDecimal,
- colors: SkontoFooterSectionColors,
- isBottomNavigationBarEnabled: Boolean,
- isSkontoSectionActive: Boolean,
- onBackClicked: () -> Unit,
- onHelpClicked: () -> Unit,
- onProceedClicked: () -> Unit,
- modifier: Modifier = Modifier,
- customBottomNavBarAdapter: InjectedViewAdapterInstance?,
- discountPercentageFormatter: SkontoDiscountPercentageFormatter,
-) {
- val animatedTotalAmount by animateFloatAsState(
- targetValue = totalAmount.value.toFloat(), label = "totalAmount"
- )
- val animatedSavedAmount by animateFloatAsState(
- targetValue = savedAmount.value.toFloat(), label = "savedAmount"
- )
- val animatedDiscountAmount by animateFloatAsState(
- targetValue = discountValue.toFloat(), label = "discountAmount"
- )
- val totalPriceText =
- "${
- currencyFormatterWithoutSymbol().format(animatedTotalAmount).trim()
- } ${totalAmount.currency.name}"
-
- val savedAmountText =
- stringResource(
- id = R.string.gbs_skonto_section_footer_label_save,
- "${
- currencyFormatterWithoutSymbol().format(animatedSavedAmount).trim()
- } ${savedAmount.currency.name}"
- )
-
- val discountLabelText = stringResource(
- id = R.string.gbs_skonto_section_footer_label_discount,
- discountPercentageFormatter.format(animatedDiscountAmount)
- )
-
- if (customBottomNavBarAdapter != null) {
- val ctx = LocalContext.current
- AndroidView(factory = {
- customBottomNavBarAdapter.viewAdapter.onCreateView(FrameLayout(ctx))
- }, update = {
- with(customBottomNavBarAdapter.viewAdapter) {
- setOnProceedClickListener(onProceedClicked)
- setOnBackClickListener(onBackClicked)
- setOnHelpClickListener(onHelpClicked)
- onTotalAmountUpdated(totalPriceText)
- onSkontoPercentageBadgeUpdated(discountLabelText)
- onSkontoPercentageBadgeVisibilityUpdate(isSkontoSectionActive)
- onSkontoSavingsAmountUpdated(savedAmountText)
- onSkontoSavingsAmountVisibilityUpdated(isSkontoSectionActive)
- }
- })
- } else {
- Card(
- modifier = modifier.fillMaxWidth(),
- shape = RectangleShape,
- colors = CardDefaults.cardColors(containerColor = colors.cardBackgroundColor),
- ) {
- Column(
- modifier = Modifier
- .tabletMaxWidth()
- .align(Alignment.CenterHorizontally),
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- Column(
- modifier = Modifier
- .padding(start = 20.dp, end = 20.dp, top = 20.dp)
- ) {
- Row {
- Text(
- modifier = Modifier.weight(0.1f),
- text = stringResource(id = R.string.gbs_skonto_section_footer_title),
- style = GiniTheme.typography.body1,
- color = colors.titleTextColor,
- )
- AnimatedVisibility(
- visible = isSkontoSectionActive
- ) {
- Box(
- modifier = Modifier
- .height(IntrinsicSize.Min)
-
- .background(
- colors.discountLabelColorScheme.backgroundColor,
- RoundedCornerShape(4.dp)
- ),
- ) {
- Text(
- modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp),
- text = discountLabelText,
- style = GiniTheme.typography.caption1,
- color = colors.discountLabelColorScheme.textColor,
- )
- }
- }
- }
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 4.dp),
- horizontalArrangement = Arrangement.Start,
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Text(
- text = totalPriceText,
- style = GiniTheme.typography.headline5.bold(),
- color = colors.amountTextColor,
- )
- }
- AnimatedVisibility(
- visible = isSkontoSectionActive
- ) {
- Text(
- text = savedAmountText,
- style = GiniTheme.typography.caption1,
- color = colors.savedAmountTextColor,
- )
- }
- }
- val buttonPaddingStart = if (isBottomNavigationBarEnabled) 16.dp else 20.dp
- val buttonPaddingEnd = if (isBottomNavigationBarEnabled) 16.dp else 20.dp
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Center
- ) {
- AnimatedVisibility(visible = isBottomNavigationBarEnabled) {
- NavigationActionBack(
- modifier = Modifier.padding(start = 16.dp),
- onClick = onBackClicked
- )
- }
- GiniButton(
- modifier = Modifier
- .weight(0.1f)
- .padding(start = buttonPaddingStart, end = buttonPaddingEnd),
- text = stringResource(id = R.string.gbs_skonto_section_footer_continue_button_text),
- onClick = onProceedClicked,
- giniButtonColors = colors.continueButtonColors
- )
- AnimatedVisibility(visible = isBottomNavigationBarEnabled) {
- NavigationActionHelp(
- modifier = Modifier.padding(end = 20.dp),
- onClick = onHelpClicked
- )
- }
- }
- }
- }
- }
-}
-
-@Composable
-@Preview
-private fun ScreenReadyStatePreviewLight() {
- ScreenReadyStatePreview()
-}
-
-@Composable
-@Preview(uiMode = UI_MODE_NIGHT_YES)
-private fun ScreenReadyStatePreviewDark() {
- ScreenReadyStatePreview()
-}
-
-@Composable
-private fun ScreenReadyStatePreview() {
- GiniTheme {
- val context = LocalContext.current
-
- var state by remember { mutableStateOf(previewState()) }
- ScreenReadyState(
- state = state,
- onDiscountSectionActiveChange = {
- state = state.copy(isSkontoSectionActive = !state.isSkontoSectionActive)
- },
- onDiscountAmountChange = {},
- onDueDateChanged = {},
- onFullAmountChange = {},
- onBackClicked = {},
- onHelpClicked = {},
- isBottomNavigationBarEnabled = false,
- onProceedClicked = {},
- customBottomNavBarAdapter = null,
- onInfoDialogDismissed = {},
- onInfoBannerClicked = {},
- onInvoiceClicked = {},
- onCancelAttachTransactionDocClicked = {
-
- },
- onConfirmAttachTransactionDocClicked = {
-
- }
- )
- }
-}
-
-private fun previewState() = SkontoFragmentContract.State.Ready(
- isSkontoSectionActive = true,
- paymentInDays = 14,
- skontoPercentage = BigDecimal("3"),
- skontoAmount = Amount.parse("97:EUR"),
- discountDueDate = LocalDate.now(),
- fullAmount = Amount.parse("100:EUR"),
- totalAmount = Amount.parse("97:EUR"),
- paymentMethod = SkontoData.SkontoPaymentMethod.PayPal,
- skontoEdgeCase = SkontoEdgeCase.PayByCashOnly,
- edgeCaseInfoDialogVisible = false,
- savedAmount = Amount.parse("3:EUR"),
- transactionDialogVisible = false,
-)
\ No newline at end of file
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragmentContract.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragmentContract.kt
deleted file mode 100644
index f66823596..000000000
--- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragmentContract.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-package net.gini.android.bank.sdk.capture.skonto
-
-import net.gini.android.bank.sdk.capture.skonto.model.SkontoData
-import net.gini.android.bank.sdk.capture.skonto.model.SkontoEdgeCase
-import net.gini.android.capture.Amount
-import java.math.BigDecimal
-import java.time.LocalDate
-
-internal object SkontoFragmentContract {
-
- sealed class State {
- data class Ready(
- val isSkontoSectionActive: Boolean,
- val paymentInDays: Int,
- val skontoPercentage: BigDecimal,
- val skontoAmount: Amount,
- val discountDueDate: LocalDate,
- val fullAmount: Amount,
- val totalAmount: Amount,
- val savedAmount: Amount,
- val paymentMethod: SkontoData.SkontoPaymentMethod,
- val skontoEdgeCase: SkontoEdgeCase?,
- val edgeCaseInfoDialogVisible: Boolean,
- val transactionDialogVisible: Boolean,
- ) : State()
- }
-
- sealed interface SideEffect {
- data class OpenInvoiceScreen(
- val documentId: String,
- val infoTextLines: List,
- ) : SideEffect
- }
-}
\ No newline at end of file
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragmentViewModel.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragmentViewModel.kt
deleted file mode 100644
index c5c90b439..000000000
--- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragmentViewModel.kt
+++ /dev/null
@@ -1,265 +0,0 @@
-package net.gini.android.bank.sdk.capture.skonto
-
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.launch
-import net.gini.android.bank.sdk.capture.extractions.skonto.SkontoExtractionsHandler
-import net.gini.android.bank.sdk.capture.skonto.factory.lines.SkontoInvoicePreviewTextLinesFactory
-import net.gini.android.bank.sdk.capture.skonto.model.SkontoData
-import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoAmountUseCase
-import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoDefaultSelectionStateUseCase
-import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoDiscountPercentageUseCase
-import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoEdgeCaseUseCase
-import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoRemainingDaysUseCase
-import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoSavedAmountUseCase
-import net.gini.android.bank.sdk.transactiondocs.internal.usecase.GetTransactionDocShouldBeAutoAttachedUseCase
-import net.gini.android.bank.sdk.transactiondocs.internal.usecase.GetTransactionDocsFeatureEnabledUseCase
-import net.gini.android.bank.sdk.transactiondocs.internal.usecase.TransactionDocDialogCancelAttachUseCase
-import net.gini.android.bank.sdk.transactiondocs.internal.usecase.TransactionDocDialogConfirmAttachUseCase
-import net.gini.android.capture.Amount
-import net.gini.android.capture.analysis.LastAnalyzedDocumentProvider
-import net.gini.android.capture.provider.LastExtractionsProvider
-import java.math.BigDecimal
-import java.time.LocalDate
-
-internal class SkontoFragmentViewModel(
- private val data: SkontoData,
- private val getSkontoDiscountPercentageUseCase: GetSkontoDiscountPercentageUseCase,
- private val getSkontoSavedAmountUseCase: GetSkontoSavedAmountUseCase,
- private val getSkontoEdgeCaseUseCase: GetSkontoEdgeCaseUseCase,
- private val getSkontoAmountUseCase: GetSkontoAmountUseCase,
- private val getSkontoRemainingDaysUseCase: GetSkontoRemainingDaysUseCase,
- private val getSkontoDefaultSelectionStateUseCase: GetSkontoDefaultSelectionStateUseCase,
- private val skontoExtractionsHandler: SkontoExtractionsHandler,
- private val lastAnalyzedDocumentProvider: LastAnalyzedDocumentProvider,
- private val skontoInvoicePreviewTextLinesFactory: SkontoInvoicePreviewTextLinesFactory,
- private val lastExtractionsProvider: LastExtractionsProvider,
- private val transactionDocDialogConfirmAttachUseCase: TransactionDocDialogConfirmAttachUseCase,
- private val transactionDocDialogCancelAttachUseCase: TransactionDocDialogCancelAttachUseCase,
- private val getTransactionDocShouldBeAutoAttachedUseCase: GetTransactionDocShouldBeAutoAttachedUseCase,
- private val getTransactionDocsFeatureEnabledUseCase: GetTransactionDocsFeatureEnabledUseCase,
-) : ViewModel() {
-
- val stateFlow: MutableStateFlow =
- MutableStateFlow(createInitalState(data))
-
- val sideEffectFlow: MutableSharedFlow = MutableSharedFlow()
-
- private var listener: SkontoFragmentListener? = null
-
- fun setListener(listener: SkontoFragmentListener?) {
- this.listener = listener
- }
-
- fun onProceedClicked() = viewModelScope.launch {
- val currentState = stateFlow.value as? SkontoFragmentContract.State.Ready ?: return@launch
- if (!getTransactionDocsFeatureEnabledUseCase()) {
- openExtractionsScreen()
- return@launch
- }
- if (getTransactionDocShouldBeAutoAttachedUseCase()) {
- onConfirmAttachTransactionDocClicked(true)
- } else {
- stateFlow.emit(currentState.copy(transactionDialogVisible = true))
- }
- }
-
- fun onConfirmAttachTransactionDocClicked(alwaysAttach: Boolean) = viewModelScope.launch {
- transactionDocDialogConfirmAttachUseCase(alwaysAttach)
- openExtractionsScreen()
- }
-
- fun onCancelAttachTransactionDocClicked() = viewModelScope.launch {
- transactionDocDialogCancelAttachUseCase()
- openExtractionsScreen()
- }
-
- private fun openExtractionsScreen() {
- val currentState = stateFlow.value as? SkontoFragmentContract.State.Ready ?: return
- skontoExtractionsHandler.updateExtractions(
- totalAmount = currentState.totalAmount,
- skontoPercentage = currentState.skontoPercentage,
- skontoAmount = currentState.skontoAmount,
- paymentInDays = currentState.paymentInDays,
- discountDueDate = currentState.discountDueDate.toString(),
- )
- lastExtractionsProvider.update(skontoExtractionsHandler.getExtractions().toMutableMap())
- listener?.onPayInvoiceWithSkonto(
- skontoExtractionsHandler.getExtractions(),
- skontoExtractionsHandler.getCompoundExtractions()
- )
- }
-
- private fun createInitalState(
- data: SkontoData,
- ): SkontoFragmentContract.State.Ready {
-
- val discount = data.skontoPercentageDiscounted
-
- val paymentMethod = data.skontoPaymentMethod ?: SkontoData.SkontoPaymentMethod.Unspecified
- val edgeCase = getSkontoEdgeCaseUseCase.execute(data.skontoDueDate, paymentMethod)
-
- val isSkontoSectionActive = getSkontoDefaultSelectionStateUseCase.execute(edgeCase)
-
- val totalAmount =
- if (isSkontoSectionActive) data.skontoAmountToPay else data.fullAmountToPay
-
- val savedAmountValue = getSkontoSavedAmountUseCase.execute(
- data.skontoAmountToPay.value,
- data.fullAmountToPay.value
- )
- val savedAmount = Amount(savedAmountValue, data.fullAmountToPay.currency)
-
- return SkontoFragmentContract.State.Ready(
- isSkontoSectionActive = isSkontoSectionActive,
- paymentInDays = data.skontoRemainingDays,
- skontoPercentage = discount,
- skontoAmount = data.skontoAmountToPay,
- discountDueDate = data.skontoDueDate,
- fullAmount = data.fullAmountToPay,
- totalAmount = totalAmount,
- paymentMethod = paymentMethod,
- skontoEdgeCase = edgeCase,
- edgeCaseInfoDialogVisible = edgeCase != null,
- savedAmount = savedAmount,
- transactionDialogVisible = false,
- )
- }
-
- fun onSkontoActiveChanged(newValue: Boolean) = viewModelScope.launch {
- val currentState = stateFlow.value as? SkontoFragmentContract.State.Ready ?: return@launch
- val totalAmount = if (newValue) currentState.skontoAmount else currentState.fullAmount
- val discount = getSkontoDiscountPercentageUseCase.execute(
- currentState.skontoAmount.value,
- currentState.fullAmount.value
- )
-
- stateFlow.emit(
- currentState.copy(
- isSkontoSectionActive = newValue,
- totalAmount = totalAmount,
- skontoPercentage = discount
- )
- )
- }
-
- fun onSkontoAmountFieldChanged(newValue: BigDecimal) = viewModelScope.launch {
- val currentState = stateFlow.value as? SkontoFragmentContract.State.Ready ?: return@launch
-
- if (newValue > currentState.fullAmount.value) {
- stateFlow.emit(
- currentState.copy(skontoAmount = currentState.skontoAmount)
- )
- return@launch
- }
-
- val discount = getSkontoDiscountPercentageUseCase.execute(
- newValue,
- currentState.fullAmount.value
- )
-
- val totalAmount = if (currentState.isSkontoSectionActive)
- newValue
- else currentState.fullAmount.value
-
- val newSkontoAmount = currentState.skontoAmount.copy(value = newValue)
- val newTotalAmount = currentState.totalAmount.copy(value = totalAmount)
-
- val savedAmountValue = getSkontoSavedAmountUseCase.execute(
- newSkontoAmount.value,
- currentState.fullAmount.value
- )
-
- val savedAmount = Amount(savedAmountValue, currentState.fullAmount.currency)
-
- stateFlow.emit(
- currentState.copy(
- skontoAmount = newSkontoAmount,
- skontoPercentage = discount,
- totalAmount = newTotalAmount,
- savedAmount = savedAmount,
- )
- )
- }
-
- fun onSkontoDueDateChanged(newDate: LocalDate) = viewModelScope.launch {
- val currentState = stateFlow.value as? SkontoFragmentContract.State.Ready ?: return@launch
- val newPayInDays = getSkontoRemainingDaysUseCase.execute(newDate)
- stateFlow.emit(
- currentState.copy(
- discountDueDate = newDate,
- paymentInDays = newPayInDays,
- skontoEdgeCase = getSkontoEdgeCaseUseCase.execute(
- dueDate = newDate,
- paymentMethod = currentState.paymentMethod
- )
- )
- )
- }
-
- fun onFullAmountFieldChanged(newValue: BigDecimal) = viewModelScope.launch {
- val currentState = stateFlow.value as? SkontoFragmentContract.State.Ready ?: return@launch
- val totalAmount =
- if (currentState.isSkontoSectionActive) currentState.skontoAmount.value else newValue
-
- val discount = currentState.skontoPercentage
-
- val skontoAmount = getSkontoAmountUseCase.execute(newValue, discount)
-
- val savedAmountValue = getSkontoSavedAmountUseCase.execute(
- skontoAmount,
- newValue
- )
-
- val savedAmount = Amount(savedAmountValue, currentState.fullAmount.currency)
-
- stateFlow.emit(
- currentState.copy(
- skontoAmount = currentState.skontoAmount.copy(value = skontoAmount),
- fullAmount = currentState.fullAmount.copy(value = newValue),
- totalAmount = currentState.totalAmount.copy(value = totalAmount),
- savedAmount = savedAmount,
- )
- )
- }
-
- fun onInfoBannerClicked() = viewModelScope.launch {
- val currentState = stateFlow.value as? SkontoFragmentContract.State.Ready ?: return@launch
- stateFlow.emit(
- currentState.copy(
- edgeCaseInfoDialogVisible = true,
- )
- )
- }
-
- fun onInfoDialogDismissed() = viewModelScope.launch {
- val currentState = stateFlow.value as? SkontoFragmentContract.State.Ready ?: return@launch
- stateFlow.emit(
- currentState.copy(
- edgeCaseInfoDialogVisible = false,
- )
- )
- }
-
- fun onInvoiceClicked() = viewModelScope.launch {
- val currentState =
- stateFlow.value as? SkontoFragmentContract.State.Ready ?: return@launch
- val skontoData = SkontoData(
- skontoAmountToPay = currentState.skontoAmount,
- skontoDueDate = currentState.discountDueDate,
- skontoPercentageDiscounted = currentState.skontoPercentage,
- skontoRemainingDays = currentState.paymentInDays,
- fullAmountToPay = currentState.fullAmount,
- skontoPaymentMethod = currentState.paymentMethod,
- )
- val documentId = lastAnalyzedDocumentProvider.provide()?.giniApiDocumentId ?: return@launch
- sideEffectFlow.emit(
- SkontoFragmentContract.SideEffect.OpenInvoiceScreen(
- documentId,
- skontoInvoicePreviewTextLinesFactory.create(skontoData)
- )
- )
- }
-}
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoScreenContent.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoScreenContent.kt
new file mode 100644
index 000000000..f5ac54aa7
--- /dev/null
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoScreenContent.kt
@@ -0,0 +1,1053 @@
+@file:OptIn(ExperimentalMaterial3Api::class)
+
+package net.gini.android.bank.sdk.capture.skonto
+
+import android.content.res.Configuration.UI_MODE_NIGHT_YES
+import android.icu.util.Calendar
+import android.widget.FrameLayout
+import androidx.activity.compose.BackHandler
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.interaction.collectIsPressedAsState
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SelectableDates
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+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.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.pluralStringResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.compose.ui.window.Dialog
+import androidx.compose.ui.window.DialogProperties
+import net.gini.android.bank.sdk.R
+import net.gini.android.bank.sdk.capture.skonto.colors.SkontoScreenColors
+import net.gini.android.bank.sdk.capture.skonto.colors.section.SkontoFooterSectionColors
+import net.gini.android.bank.sdk.capture.skonto.colors.section.SkontoInfoDialogColors
+import net.gini.android.bank.sdk.capture.skonto.colors.section.SkontoInvoicePreviewSectionColors
+import net.gini.android.bank.sdk.capture.skonto.colors.section.SkontoSectionColors
+import net.gini.android.bank.sdk.capture.skonto.colors.section.WithoutSkontoSectionColors
+import net.gini.android.bank.sdk.capture.skonto.formatter.AmountFormatter
+import net.gini.android.bank.sdk.capture.skonto.formatter.SkontoDiscountPercentageFormatter
+import net.gini.android.bank.sdk.capture.skonto.mapper.toErrorMessage
+import net.gini.android.bank.sdk.capture.skonto.model.SkontoData
+import net.gini.android.bank.sdk.capture.skonto.model.SkontoEdgeCase
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.SkontoFragmentViewModel
+import net.gini.android.bank.sdk.capture.util.currencyFormatterWithoutSymbol
+import net.gini.android.bank.sdk.transactiondocs.ui.dialog.attachdoc.AttachDocumentToTransactionDialog
+import net.gini.android.bank.sdk.util.ui.keyboardAsState
+import net.gini.android.capture.Amount
+import net.gini.android.capture.ui.components.button.filled.GiniButton
+import net.gini.android.capture.ui.components.picker.date.GiniDatePickerDialog
+import net.gini.android.capture.ui.components.switcher.GiniSwitch
+import net.gini.android.capture.ui.components.textinput.GiniTextInput
+import net.gini.android.capture.ui.components.textinput.amount.GiniAmountTextInput
+import net.gini.android.capture.ui.components.tooltip.GiniTooltipBox
+import net.gini.android.capture.ui.components.topbar.GiniTopBar
+import net.gini.android.capture.ui.components.topbar.GiniTopBarColors
+import net.gini.android.capture.ui.theme.GiniTheme
+import net.gini.android.capture.ui.theme.modifier.tabletMaxWidth
+import net.gini.android.capture.ui.theme.typography.bold
+import net.gini.android.capture.util.compose.keyboardPadding
+import net.gini.android.capture.view.InjectedViewAdapterInstance
+import org.orbitmvi.orbit.compose.collectAsState
+import org.orbitmvi.orbit.compose.collectSideEffect
+import java.math.BigDecimal
+import java.time.LocalDate
+import java.time.format.DateTimeFormatter
+
+
+@Composable
+internal fun SkontoScreenContent(
+ isBottomNavigationBarEnabled: Boolean,
+ navigateBack: () -> Unit,
+ navigateToHelp: () -> Unit,
+ amountFormatter: AmountFormatter,
+ viewModel: SkontoFragmentViewModel,
+ customBottomNavBarAdapter: InjectedViewAdapterInstance?,
+ navigateToInvoiceScreen: (documentId: String, infoTextLines: List) -> Unit,
+ modifier: Modifier = Modifier,
+ screenColorScheme: SkontoScreenColors = SkontoScreenColors.colors(),
+) {
+
+ BackHandler { navigateBack() }
+
+ val state by viewModel.collectAsState()
+ viewModel.collectSideEffect {
+ when (it) {
+ is SkontoScreenSideEffect.OpenInvoiceScreen ->
+ navigateToInvoiceScreen(it.documentId, it.infoTextLines)
+ }
+ }
+
+ val keyboardState by keyboardAsState()
+
+ LaunchedEffect(keyboardState) {
+ viewModel.onKeyboardStateChanged(keyboardState)
+ }
+
+ ScreenStateContent(
+ modifier = modifier,
+ state = state,
+ screenColorScheme = screenColorScheme,
+ onDiscountSectionActiveChange = viewModel::onSkontoActiveChanged,
+ onSkontoAmountChange = viewModel::onSkontoAmountFieldChanged,
+ onDueDateChanged = viewModel::onSkontoDueDateChanged,
+ onFullAmountChange = viewModel::onFullAmountFieldChanged,
+ isBottomNavigationBarEnabled = isBottomNavigationBarEnabled,
+ onBackClicked = navigateBack,
+ onHelpClicked = navigateToHelp,
+ customBottomNavBarAdapter = customBottomNavBarAdapter,
+ onProceedClicked = viewModel::onProceedClicked,
+ onInfoBannerClicked = viewModel::onInfoBannerClicked,
+ onInfoDialogDismissed = viewModel::onInfoDialogDismissed,
+ onInvoiceClicked = viewModel::onInvoiceClicked,
+ onConfirmAttachTransactionDocClicked = viewModel::onConfirmAttachTransactionDocClicked,
+ onCancelAttachTransactionDocClicked = viewModel::onCancelAttachTransactionDocClicked,
+ amountFormatter = amountFormatter,
+ )
+}
+
+@Composable
+private fun ScreenStateContent(
+ state: SkontoScreenState,
+ amountFormatter: AmountFormatter,
+ onDiscountSectionActiveChange: (Boolean) -> Unit,
+ onSkontoAmountChange: (BigDecimal) -> Unit,
+ onFullAmountChange: (BigDecimal) -> Unit,
+ onDueDateChanged: (LocalDate) -> Unit,
+ onBackClicked: () -> Unit,
+ onHelpClicked: () -> Unit,
+ onProceedClicked: () -> Unit,
+ isBottomNavigationBarEnabled: Boolean,
+ onInfoBannerClicked: () -> Unit,
+ onInfoDialogDismissed: () -> Unit,
+ onInvoiceClicked: () -> Unit,
+ onConfirmAttachTransactionDocClicked: (alwaysAttach: Boolean) -> Unit,
+ onCancelAttachTransactionDocClicked: () -> Unit,
+ customBottomNavBarAdapter: InjectedViewAdapterInstance?,
+ modifier: Modifier = Modifier,
+ screenColorScheme: SkontoScreenColors = SkontoScreenColors.colors()
+) {
+ when (state) {
+ is SkontoScreenState.Ready -> ScreenReadyState(
+ amountFormatter = amountFormatter,
+ modifier = modifier,
+ state = state,
+ screenColorScheme = screenColorScheme,
+ onDiscountSectionActiveChange = onDiscountSectionActiveChange,
+ onDiscountAmountChange = onSkontoAmountChange,
+ onDueDateChanged = onDueDateChanged,
+ onFullAmountChange = onFullAmountChange,
+ onBackClicked = onBackClicked,
+ onHelpClicked = onHelpClicked,
+ isBottomNavigationBarEnabled = isBottomNavigationBarEnabled,
+ customBottomNavBarAdapter = customBottomNavBarAdapter,
+ onProceedClicked = onProceedClicked,
+ onInfoBannerClicked = onInfoBannerClicked,
+ onInfoDialogDismissed = onInfoDialogDismissed,
+ onInvoiceClicked = onInvoiceClicked,
+ onConfirmAttachTransactionDocClicked = onConfirmAttachTransactionDocClicked,
+ onCancelAttachTransactionDocClicked = onCancelAttachTransactionDocClicked,
+ )
+ }
+
+}
+
+@Composable
+private fun ScreenReadyState(
+ isBottomNavigationBarEnabled: Boolean,
+ state: SkontoScreenState.Ready,
+ amountFormatter: AmountFormatter,
+ onConfirmAttachTransactionDocClicked: (alwaysAttach: Boolean) -> Unit,
+ onCancelAttachTransactionDocClicked: () -> Unit,
+ onBackClicked: () -> Unit,
+ onHelpClicked: () -> Unit,
+ onProceedClicked: () -> Unit,
+ onInvoiceClicked: () -> Unit,
+ onDiscountSectionActiveChange: (Boolean) -> Unit,
+ onDiscountAmountChange: (BigDecimal) -> Unit,
+ onDueDateChanged: (LocalDate) -> Unit,
+ onFullAmountChange: (BigDecimal) -> Unit,
+ onInfoBannerClicked: () -> Unit,
+ onInfoDialogDismissed: () -> Unit,
+ customBottomNavBarAdapter: InjectedViewAdapterInstance?,
+ modifier: Modifier = Modifier,
+ discountPercentageFormatter: SkontoDiscountPercentageFormatter = SkontoDiscountPercentageFormatter(),
+ screenColorScheme: SkontoScreenColors = SkontoScreenColors.colors(),
+) {
+ val scrollState = rememberScrollState()
+ val keyboardPadding by keyboardPadding(108.dp, scrollState)
+
+ Scaffold(modifier = modifier,
+ containerColor = screenColorScheme.backgroundColor,
+ topBar = {
+ TopAppBar(
+ isBottomNavigationBarEnabled = isBottomNavigationBarEnabled,
+ colors = screenColorScheme.topAppBarColors,
+ onBackClicked = onBackClicked,
+ onHelpClicked = onHelpClicked
+ )
+ },
+ bottomBar = {
+ FooterSection(
+ colors = screenColorScheme.footerSectionColors,
+ discountValue = state.skontoPercentage,
+ totalAmount = state.totalAmount,
+ isBottomNavigationBarEnabled = isBottomNavigationBarEnabled,
+ onBackClicked = onBackClicked,
+ onHelpClicked = onHelpClicked,
+ customBottomNavBarAdapter = customBottomNavBarAdapter,
+ onProceedClicked = onProceedClicked,
+ isSkontoSectionActive = state.isSkontoSectionActive,
+ savedAmount = state.savedAmount,
+ discountPercentageFormatter = discountPercentageFormatter,
+ )
+ }) {
+ Column(
+ modifier = Modifier
+ .padding(it)
+ .verticalScroll(scrollState)
+ .fillMaxSize()
+ .padding(bottom = keyboardPadding),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ val invoicePreviewPaddingTop =
+ if (LocalContext.current.resources.getBoolean(net.gini.android.capture.R.bool.gc_is_tablet)) {
+ 64.dp
+ } else {
+ 8.dp
+ }
+ InvoicePreviewSection(
+ modifier = Modifier
+ .padding(top = invoicePreviewPaddingTop)
+ .tabletMaxWidth(),
+ colorScheme = screenColorScheme.invoiceScanSectionColors,
+ onClick = onInvoiceClicked,
+ )
+ SkontoSection(
+ amountFormatter = amountFormatter,
+ modifier = Modifier
+ .padding(top = 8.dp)
+ .tabletMaxWidth(),
+ colors = screenColorScheme.skontoSectionColors,
+ amount = state.skontoAmount,
+ dueDate = state.discountDueDate,
+ infoPaymentInDays = state.paymentInDays,
+ infoDiscountValue = state.skontoPercentage,
+ onActiveChange = onDiscountSectionActiveChange,
+ isActive = state.isSkontoSectionActive,
+ onSkontoAmountChange = onDiscountAmountChange,
+ onDueDateChanged = onDueDateChanged,
+ edgeCase = state.edgeCase,
+ onInfoBannerClicked = onInfoBannerClicked,
+ discountPercentageFormatter = discountPercentageFormatter,
+ skontoAmountValidationError = state.skontoAmountValidationError,
+ )
+ WithoutSkontoSection(
+ modifier = Modifier.tabletMaxWidth(),
+ colors = screenColorScheme.withoutSkontoSectionColors,
+ isActive = !state.isSkontoSectionActive,
+ amount = state.fullAmount,
+ onFullAmountChange = onFullAmountChange,
+ amountFormatter = amountFormatter,
+ fullAmountValidationError = state.fullAmountValidationError,
+ )
+ }
+ }
+
+ if (state.edgeCaseInfoDialogVisible) {
+ val text = when (state.edgeCase) {
+ SkontoEdgeCase.PayByCashToday,
+ SkontoEdgeCase.PayByCashOnly ->
+ stringResource(id = R.string.gbs_skonto_section_info_dialog_pay_cash_message)
+
+ SkontoEdgeCase.SkontoExpired ->
+ stringResource(
+ id = R.string.gbs_skonto_section_info_dialog_date_expired_message,
+ discountPercentageFormatter.format(state.skontoPercentage.toFloat())
+ )
+
+ SkontoEdgeCase.SkontoLastDay ->
+ stringResource(
+ id = R.string.gbs_skonto_section_info_dialog_pay_today_message,
+ )
+
+ null -> ""
+ }
+ InfoDialog(
+ text = text,
+ colors = screenColorScheme.infoDialogColors,
+ onDismissRequest = onInfoDialogDismissed
+ )
+ }
+
+ if (state.transactionDialogVisible) {
+ AttachDocumentToTransactionDialog(
+ onDismiss = onCancelAttachTransactionDocClicked,
+ onConfirm = onConfirmAttachTransactionDocClicked
+ )
+ }
+ }
+}
+
+@Composable
+private fun TopAppBar(
+ onBackClicked: () -> Unit,
+ onHelpClicked: () -> Unit,
+ colors: GiniTopBarColors,
+ isBottomNavigationBarEnabled: Boolean,
+ modifier: Modifier = Modifier,
+) {
+ GiniTopBar(
+ modifier = modifier,
+ colors = colors,
+ title = stringResource(id = R.string.gbs_skonto_screen_title),
+ navigationIcon = {
+ AnimatedVisibility(visible = !isBottomNavigationBarEnabled) {
+ NavigationActionBack(
+ modifier = Modifier.padding(start = 16.dp, end = 32.dp),
+ onClick = onBackClicked
+ )
+ }
+ },
+ actions = {
+ AnimatedVisibility(visible = !isBottomNavigationBarEnabled) {
+ NavigationActionHelp(
+ modifier = Modifier.padding(start = 20.dp, end = 12.dp),
+ onClick = onHelpClicked
+ )
+ }
+ })
+}
+
+@Composable
+private fun NavigationActionHelp(
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ GiniTooltipBox(
+ tooltipText = stringResource(
+ id = R.string.gbs_skonto_screen_content_description_help
+ )
+ ) {
+ IconButton(
+ modifier = modifier
+ .width(24.dp)
+ .height(24.dp),
+ onClick = onClick
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.gbs_help_question_icon),
+ contentDescription = stringResource(
+ id = R.string.gbs_skonto_screen_content_description_help
+ ),
+ )
+ }
+ }
+}
+
+@Composable
+private fun NavigationActionBack(
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ GiniTooltipBox(
+ tooltipText = stringResource(
+ id = R.string.gbs_skonto_screen_content_description_back
+ )
+ ) {
+ IconButton(
+ modifier = modifier
+ .width(24.dp)
+ .height(24.dp),
+ onClick = onClick
+ ) {
+ Icon(
+ painter = painterResource(id = net.gini.android.capture.R.drawable.gc_action_bar_back),
+ contentDescription = stringResource(
+ id = R.string.gbs_skonto_screen_content_description_back
+ ),
+ )
+ }
+ }
+}
+
+@Composable
+private fun InvoicePreviewSection(
+ onClick: () -> Unit,
+ colorScheme: SkontoInvoicePreviewSectionColors,
+ modifier: Modifier = Modifier,
+) {
+ Card(
+ modifier = modifier
+ .fillMaxWidth()
+ .clickable(onClick = onClick),
+ shape = RectangleShape,
+ colors = CardDefaults.cardColors(containerColor = colorScheme.cardBackgroundColor)
+ ) {
+ Row(
+ modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically
+ ) {
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier
+ .background(colorScheme.iconBackgroundColor, shape = RoundedCornerShape(4.dp))
+ ) {
+ Icon(
+ modifier = Modifier
+ .size(40.dp)
+ .padding(8.dp),
+ painter = painterResource(id = R.drawable.gbs_icon_document),
+ contentDescription = null,
+ tint = colorScheme.iconTint,
+ )
+ }
+
+ Column(
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .fillMaxWidth()
+ .weight(0.1f)
+ ) {
+ Text(
+ text = stringResource(id = R.string.gbs_skonto_section_invoice_preview_title),
+ style = GiniTheme.typography.subtitle1,
+ color = colorScheme.titleTextColor
+ )
+ Text(
+ text = stringResource(id = R.string.gbs_skonto_invoice_section_preview_subtitle),
+ style = GiniTheme.typography.body2,
+ color = colorScheme.subtitleTextColor
+ )
+ }
+
+ Icon(
+ painter = painterResource(id = R.drawable.gbs_arrow_right),
+ contentDescription = null,
+ tint = colorScheme.arrowTint
+ )
+ }
+
+ }
+}
+
+@Composable
+private fun SkontoSection(
+ isActive: Boolean,
+ amountFormatter: AmountFormatter,
+ colors: SkontoSectionColors,
+ amount: Amount,
+ dueDate: LocalDate,
+ infoPaymentInDays: Int,
+ infoDiscountValue: BigDecimal,
+ onActiveChange: (Boolean) -> Unit,
+ onSkontoAmountChange: (BigDecimal) -> Unit,
+ onDueDateChanged: (LocalDate) -> Unit,
+ onInfoBannerClicked: () -> Unit,
+ edgeCase: SkontoEdgeCase?,
+ skontoAmountValidationError: SkontoScreenState.Ready.SkontoAmountValidationError?,
+ modifier: Modifier = Modifier,
+ discountPercentageFormatter: SkontoDiscountPercentageFormatter = SkontoDiscountPercentageFormatter(),
+) {
+ val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy")
+ val resources = LocalContext.current.resources
+
+ var isDatePickerVisible by remember { mutableStateOf(false) }
+ Card(
+ modifier = modifier,
+ shape = RectangleShape,
+ colors = CardDefaults.cardColors(containerColor = colors.cardBackgroundColor)
+ ) {
+ Column(
+ modifier = Modifier.padding(16.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.Start,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(
+ text = stringResource(id = R.string.gbs_skonto_section_discount_title),
+ style = GiniTheme.typography.subtitle1,
+ color = colors.titleTextColor,
+ )
+ Box {
+ androidx.compose.animation.AnimatedVisibility(visible = isActive) {
+ Text(
+ text = stringResource(id = R.string.gbs_skonto_section_discount_hint_label_enabled),
+ style = GiniTheme.typography.subtitle2,
+ color = colors.enabledHintTextColor,
+ )
+ }
+ }
+
+ Spacer(Modifier.weight(1f))
+
+
+ GiniSwitch(
+ checked = isActive,
+ onCheckedChange = onActiveChange,
+ )
+ }
+ val animatedDiscountAmount by animateFloatAsState(
+ targetValue = infoDiscountValue.toFloat(),
+ label = "discountAmount"
+ )
+
+ val remainingDaysText =
+ if (infoPaymentInDays != 0) {
+ pluralStringResource(
+ id = R.plurals.days,
+ count = infoPaymentInDays,
+ infoPaymentInDays.toString()
+ )
+ } else {
+ stringResource(id = R.string.days_zero)
+ }
+
+ val infoBannerText = when (edgeCase) {
+ SkontoEdgeCase.PayByCashOnly ->
+ stringResource(
+ id = R.string.gbs_skonto_section_discount_info_banner_pay_cash_message,
+ discountPercentageFormatter.format(animatedDiscountAmount),
+ remainingDaysText
+ )
+
+ SkontoEdgeCase.PayByCashToday ->
+ stringResource(
+ id = R.string.gbs_skonto_section_discount_info_banner_pay_cash_today_message,
+ discountPercentageFormatter.format(animatedDiscountAmount)
+ )
+
+ SkontoEdgeCase.SkontoExpired ->
+ stringResource(
+ id = R.string.gbs_skonto_section_discount_info_banner_date_expired_message,
+ discountPercentageFormatter.format(animatedDiscountAmount)
+ )
+
+ SkontoEdgeCase.SkontoLastDay ->
+ stringResource(
+ id = R.string.gbs_skonto_section_discount_info_banner_pay_today_message,
+ discountPercentageFormatter.format(animatedDiscountAmount)
+ )
+
+ else -> stringResource(
+ id = R.string.gbs_skonto_section_discount_info_banner_normal_message,
+ remainingDaysText,
+ discountPercentageFormatter.format(animatedDiscountAmount)
+ )
+ }
+
+ InfoBanner(
+ text = infoBannerText,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 6.dp),
+ colors = when (edgeCase) {
+ SkontoEdgeCase.SkontoLastDay,
+ SkontoEdgeCase.PayByCashToday,
+ SkontoEdgeCase.PayByCashOnly -> colors.warningInfoBannerColors
+
+ SkontoEdgeCase.SkontoExpired -> colors.errorInfoBannerColors
+ else -> colors.successInfoBannerColors
+ },
+ onClicked = onInfoBannerClicked,
+ clickable = edgeCase != null,
+ )
+ GiniAmountTextInput(
+ amount = amount.value,
+ currencyCode = amount.currency.name,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 16.dp),
+ enabled = isActive,
+ colors = colors.amountFieldColors,
+ onValueChange = { onSkontoAmountChange(it) },
+ label = stringResource(id = R.string.gbs_skonto_section_discount_field_amount_hint),
+ trailingContent = {
+ AnimatedVisibility(visible = isActive) {
+ Text(
+ text = amount.currency.name,
+ style = GiniTheme.typography.subtitle1,
+ )
+ }
+ },
+ isError = skontoAmountValidationError != null,
+ supportingText = skontoAmountValidationError?.toErrorMessage(
+ resources = resources,
+ amountFormatter = amountFormatter
+ )
+ )
+
+ val dueDateOnClickSource = remember { MutableInteractionSource() }
+ val pressed by dueDateOnClickSource.collectIsPressedAsState()
+
+ LaunchedEffect(key1 = pressed) {
+ if (pressed) {
+ isDatePickerVisible = true
+ }
+ }
+
+ GiniTextInput(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 16.dp)
+ .focusable(false),
+ enabled = isActive,
+ interactionSource = dueDateOnClickSource,
+ readOnly = true,
+ colors = colors.dueDateTextFieldColor,
+ onValueChange = { /* Ignored */ },
+ text = dueDate.format(dateFormatter),
+ label = stringResource(id = R.string.gbs_skonto_section_discount_field_due_date_hint),
+ trailingContent = {
+ androidx.compose.animation.AnimatedVisibility(visible = isActive) {
+ Icon(
+ painter = painterResource(id = R.drawable.gbs_icon_calendar),
+ contentDescription = null,
+ )
+ }
+ },
+ )
+ }
+ }
+
+ if (isDatePickerVisible) {
+ GiniDatePickerDialog(
+ onDismissRequest = { isDatePickerVisible = false },
+ onSaved = {
+ isDatePickerVisible = false
+ onDueDateChanged(it)
+ },
+ date = dueDate,
+ selectableDates = getSkontoSelectableDates()
+ )
+ }
+}
+
+private fun getSkontoSelectableDates() = object : SelectableDates {
+
+ val minDateCalendar = Calendar.getInstance().apply {
+ set(Calendar.MILLISECONDS_IN_DAY, 0)
+ }
+
+ val maxDateCalendar = Calendar.getInstance().apply {
+ add(Calendar.MONTH, 6)
+ }
+
+ val minTime = minDateCalendar.timeInMillis
+ val maxTime = maxDateCalendar.timeInMillis
+
+ override fun isSelectableDate(utcTimeMillis: Long): Boolean {
+ return (minTime..maxTime).contains(utcTimeMillis)
+ }
+
+ override fun isSelectableYear(year: Int): Boolean {
+ return (minDateCalendar.get(Calendar.YEAR)..maxDateCalendar.get(Calendar.YEAR))
+ .contains(year)
+ }
+}
+
+@Composable
+private fun InfoBanner(
+ colors: SkontoSectionColors.InfoBannerColors,
+ text: String,
+ clickable: Boolean,
+ onClicked: () -> Unit,
+ modifier: Modifier = Modifier,
+ icon: Painter = painterResource(id = R.drawable.gbs_icon_important_info),
+) {
+ Row(
+ modifier = modifier
+ .background(
+ color = colors.backgroundColor, RoundedCornerShape(8.dp)
+ )
+ .clickable(onClick = onClicked, enabled = clickable),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Icon(
+ modifier = Modifier.padding(8.dp),
+ painter = icon,
+ contentDescription = null,
+ tint = colors.iconTint,
+ )
+
+ Text(
+ modifier = Modifier.padding(top = 12.dp, bottom = 12.dp, end = 16.dp),
+ text = text,
+ style = GiniTheme.typography.subtitle2,
+ color = colors.textColor,
+ )
+ }
+}
+
+@Composable
+private fun InfoDialog(
+ text: String,
+ colors: SkontoInfoDialogColors,
+ onDismissRequest: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Dialog(
+ properties = DialogProperties(),
+ onDismissRequest = onDismissRequest
+ ) {
+ Card(
+ modifier = modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(28.dp),
+ colors = CardDefaults.cardColors(
+ containerColor = colors.cardBackgroundColor
+ )
+ ) {
+ Text(
+ modifier = Modifier.padding(top = 24.dp, start = 16.dp, end = 16.dp),
+ text = text,
+ style = GiniTheme.typography.caption1
+ )
+ Button(
+ modifier = Modifier
+ .padding(16.dp)
+ .align(Alignment.End),
+ onClick = onDismissRequest,
+ shape = RoundedCornerShape(4.dp),
+ colors = ButtonDefaults.textButtonColors(
+ contentColor = colors.buttonTextColor,
+ ),
+ ) {
+ Text(
+ modifier = Modifier,
+ text = stringResource(id = R.string.gbs_skonto_section_info_dialog_ok_button_text),
+ style = GiniTheme.typography.button
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun WithoutSkontoSection(
+ isActive: Boolean,
+ onFullAmountChange: (BigDecimal) -> Unit,
+ colors: WithoutSkontoSectionColors,
+ amount: Amount,
+ amountFormatter: AmountFormatter,
+ fullAmountValidationError: SkontoScreenState.Ready.FullAmountValidationError?,
+ modifier: Modifier = Modifier,
+) {
+ val resources = LocalContext.current.resources
+
+ Card(
+ modifier = modifier,
+ shape = RectangleShape,
+ colors = CardDefaults.cardColors(containerColor = colors.cardBackgroundColor)
+ ) {
+ Column(
+ modifier = Modifier.padding(16.dp)
+ ) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.Start,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(
+ text = stringResource(id = R.string.gbs_skonto_section_without_discount_title),
+ style = GiniTheme.typography.subtitle1,
+ color = colors.titleTextColor,
+ )
+ AnimatedVisibility(visible = isActive) {
+ Text(
+ text = stringResource(id = R.string.gbs_skonto_section_discount_hint_label_enabled),
+ modifier = Modifier.weight(0.1f),
+ style = GiniTheme.typography.subtitle2,
+ color = colors.enabledHintTextColor,
+ )
+ }
+ }
+ GiniAmountTextInput(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 16.dp),
+ enabled = isActive,
+ colors = colors.amountFieldColors,
+ amount = amount.value,
+ currencyCode = amount.currency.name,
+ onValueChange = onFullAmountChange,
+ label = stringResource(id = R.string.gbs_skonto_section_without_discount_field_amount_hint),
+ trailingContent = {
+ AnimatedVisibility(visible = isActive) {
+ Text(
+ text = amount.currency.name,
+ style = GiniTheme.typography.subtitle1,
+ )
+ }
+ },
+ isError = fullAmountValidationError != null,
+ supportingText = fullAmountValidationError?.toErrorMessage(
+ resources = resources,
+ amountFormatter = amountFormatter
+ )
+ )
+ }
+ }
+}
+
+@Composable
+private fun FooterSection(
+ totalAmount: Amount,
+ savedAmount: Amount,
+ discountValue: BigDecimal,
+ colors: SkontoFooterSectionColors,
+ isBottomNavigationBarEnabled: Boolean,
+ isSkontoSectionActive: Boolean,
+ onBackClicked: () -> Unit,
+ onHelpClicked: () -> Unit,
+ onProceedClicked: () -> Unit,
+ discountPercentageFormatter: SkontoDiscountPercentageFormatter,
+ customBottomNavBarAdapter: InjectedViewAdapterInstance?,
+ modifier: Modifier = Modifier,
+) {
+ val animatedTotalAmount by animateFloatAsState(
+ targetValue = totalAmount.value.toFloat(), label = "totalAmount"
+ )
+ val animatedSavedAmount by animateFloatAsState(
+ targetValue = savedAmount.value.toFloat(), label = "savedAmount"
+ )
+ val animatedDiscountAmount by animateFloatAsState(
+ targetValue = discountValue.toFloat(), label = "discountAmount"
+ )
+ val totalPriceText =
+ "${
+ currencyFormatterWithoutSymbol().format(animatedTotalAmount).trim()
+ } ${totalAmount.currency.name}"
+
+ val savedAmountText =
+ stringResource(
+ id = R.string.gbs_skonto_section_footer_label_save,
+ "${
+ currencyFormatterWithoutSymbol().format(animatedSavedAmount).trim()
+ } ${savedAmount.currency.name}"
+ )
+
+ val discountLabelText = stringResource(
+ id = R.string.gbs_skonto_section_footer_label_discount,
+ discountPercentageFormatter.format(animatedDiscountAmount)
+ )
+
+ if (customBottomNavBarAdapter != null) {
+ val ctx = LocalContext.current
+ AndroidView(factory = {
+ customBottomNavBarAdapter.viewAdapter.onCreateView(FrameLayout(ctx))
+ }, update = {
+ with(customBottomNavBarAdapter.viewAdapter) {
+ setOnProceedClickListener(onProceedClicked)
+ setOnBackClickListener(onBackClicked)
+ setOnHelpClickListener(onHelpClicked)
+ onTotalAmountUpdated(totalPriceText)
+ onSkontoPercentageBadgeUpdated(discountLabelText)
+ onSkontoPercentageBadgeVisibilityUpdate(isSkontoSectionActive)
+ onSkontoSavingsAmountUpdated(savedAmountText)
+ onSkontoSavingsAmountVisibilityUpdated(isSkontoSectionActive)
+ }
+ })
+ } else {
+ Card(
+ modifier = modifier.fillMaxWidth(),
+ shape = RectangleShape,
+ colors = CardDefaults.cardColors(containerColor = colors.cardBackgroundColor),
+ ) {
+ Column(
+ modifier = Modifier
+ .tabletMaxWidth()
+ .align(Alignment.CenterHorizontally),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Column(
+ modifier = Modifier
+ .padding(start = 20.dp, end = 20.dp, top = 20.dp)
+ ) {
+ Row {
+ Text(
+ modifier = Modifier.weight(0.1f),
+ text = stringResource(id = R.string.gbs_skonto_section_footer_title),
+ style = GiniTheme.typography.body1,
+ color = colors.titleTextColor,
+ )
+ AnimatedVisibility(
+ visible = isSkontoSectionActive
+ ) {
+ Box(
+ modifier = Modifier
+ .height(IntrinsicSize.Min)
+
+ .background(
+ colors.discountLabelColorScheme.backgroundColor,
+ RoundedCornerShape(4.dp)
+ ),
+ ) {
+ Text(
+ modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp),
+ text = discountLabelText,
+ style = GiniTheme.typography.caption1,
+ color = colors.discountLabelColorScheme.textColor,
+ )
+ }
+ }
+ }
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 4.dp),
+ horizontalArrangement = Arrangement.Start,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Text(
+ text = totalPriceText,
+ style = GiniTheme.typography.headline5.bold(),
+ color = colors.amountTextColor,
+ )
+ }
+ AnimatedVisibility(
+ visible = isSkontoSectionActive
+ ) {
+ Text(
+ text = savedAmountText,
+ style = GiniTheme.typography.caption1,
+ color = colors.savedAmountTextColor,
+ )
+ }
+ }
+ val buttonPaddingStart = if (isBottomNavigationBarEnabled) 16.dp else 20.dp
+ val buttonPaddingEnd = if (isBottomNavigationBarEnabled) 16.dp else 20.dp
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center
+ ) {
+ AnimatedVisibility(visible = isBottomNavigationBarEnabled) {
+ NavigationActionBack(
+ modifier = Modifier.padding(start = 16.dp),
+ onClick = onBackClicked
+ )
+ }
+ GiniButton(
+ modifier = Modifier
+ .weight(0.1f)
+ .padding(start = buttonPaddingStart, end = buttonPaddingEnd),
+ text = stringResource(id = R.string.gbs_skonto_section_footer_continue_button_text),
+ onClick = onProceedClicked,
+ giniButtonColors = colors.continueButtonColors
+ )
+ AnimatedVisibility(visible = isBottomNavigationBarEnabled) {
+ NavigationActionHelp(
+ modifier = Modifier.padding(end = 20.dp),
+ onClick = onHelpClicked
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+@Preview
+private fun ScreenReadyStatePreviewLight() {
+ ScreenReadyStatePreview()
+}
+
+@Composable
+@Preview(uiMode = UI_MODE_NIGHT_YES)
+private fun ScreenReadyStatePreviewDark() {
+ ScreenReadyStatePreview()
+}
+
+@Composable
+private fun ScreenReadyStatePreview() {
+ GiniTheme {
+ var state by remember { mutableStateOf(previewState()) }
+ ScreenReadyState(
+ state = state,
+ onDiscountSectionActiveChange = {
+ state = state.copy(isSkontoSectionActive = !state.isSkontoSectionActive)
+ },
+ onDiscountAmountChange = {},
+ onDueDateChanged = {},
+ onFullAmountChange = {},
+ onBackClicked = {},
+ onHelpClicked = {},
+ isBottomNavigationBarEnabled = true,
+ onProceedClicked = {},
+ customBottomNavBarAdapter = null,
+ onInfoDialogDismissed = {},
+ onInfoBannerClicked = {},
+ onInvoiceClicked = {},
+ onCancelAttachTransactionDocClicked = {
+
+ },
+ onConfirmAttachTransactionDocClicked = {
+
+ },
+ amountFormatter = AmountFormatter(currencyFormatterWithoutSymbol())
+ )
+ }
+}
+
+private fun previewState() = SkontoScreenState.Ready(
+ isSkontoSectionActive = true,
+ paymentInDays = 14,
+ skontoPercentage = BigDecimal("3"),
+ skontoAmount = Amount.parse("97:EUR"),
+ discountDueDate = LocalDate.now(),
+ fullAmount = Amount.parse("100:EUR"),
+ totalAmount = Amount.parse("97:EUR"),
+ paymentMethod = SkontoData.SkontoPaymentMethod.PayPal,
+ edgeCase = SkontoEdgeCase.PayByCashOnly,
+ edgeCaseInfoDialogVisible = false,
+ savedAmount = Amount.parse("3:EUR"),
+ transactionDialogVisible = true,
+ skontoAmountValidationError = null,
+ fullAmountValidationError = null,
+)
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoScreenModule.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoScreenModule.kt
index 047686259..993404d97 100644
--- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoScreenModule.kt
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoScreenModule.kt
@@ -2,6 +2,20 @@ package net.gini.android.bank.sdk.capture.skonto
import net.gini.android.bank.sdk.capture.skonto.factory.lines.SkontoInvoicePreviewTextLinesFactory
import net.gini.android.bank.sdk.capture.skonto.model.SkontoData
+import net.gini.android.bank.sdk.capture.skonto.validation.SkontoAmountValidator
+import net.gini.android.bank.sdk.capture.skonto.validation.SkontoFullAmountValidator
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.SkontoFragmentViewModel
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.SkontoScreenInitialStateFactory
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.intent.FullAmountChangeIntent
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.intent.InfoBannerInteractionIntent
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.intent.InvoiceClickIntent
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.intent.KeyboardStateChangeIntent
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.intent.ProceedClickedIntent
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.intent.SkontoActiveChangeIntent
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.intent.SkontoAmountFieldChangeIntent
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.intent.SkontoDueDateChangeIntent
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.intent.TransactionDocDialogDecisionIntent
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.subintent.OpenExtractionsScreenSubIntent
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
@@ -10,26 +24,93 @@ val skontoScreenModule = module {
viewModel { (data: SkontoData) ->
SkontoFragmentViewModel(
data = data,
+ skontoScreenInitialStateFactory = get(),
+ proceedClickedIntent = get(),
+ skontoActiveChangeIntent = get(),
+ keyboardStateChangeIntent = get(),
+ skontoAmountFieldChangeIntent = get(),
+ invoiceClickIntent = get(),
+ fullAmountChangeIntent = get(),
+ skontoDueDateChangeIntent = get(),
+ transactionDocDialogDecisionIntent = get(),
+ infoBannerInteractionIntent = get()
+ )
+ }
+ factory {
+ SkontoInvoicePreviewTextLinesFactory(
+ resources = androidContext().resources,
+ amountFormatter = get()
+ )
+ }
+ factory {
+ SkontoAmountValidator()
+ }
+ factory {
+ SkontoFullAmountValidator()
+ }
+ factory {
+ SkontoScreenInitialStateFactory(
+ getSkontoSavedAmountUseCase = get(),
+ getSkontoEdgeCaseUseCase = get(),
+ getSkontoDefaultSelectionStateUseCase = get()
+ )
+ }
+ factory {
+ ProceedClickedIntent(
+ openExtractionsScreenSubIntent = get(),
+ getTransactionDocShouldBeAutoAttachedUseCase = get(),
+ getTransactionDocsFeatureEnabledUseCase = get(),
+ transactionDocDialogConfirmAttachUseCase = get()
+ )
+ }
+ factory {
+ InvoiceClickIntent(
+ lastAnalyzedDocumentProvider = get(),
+ skontoInvoicePreviewTextLinesFactory = get()
+ )
+ }
+ factory {
+ FullAmountChangeIntent(
+ skontoFullAmountValidator = get(),
getSkontoAmountUseCase = get(),
+ getSkontoSavedAmountUseCase = get()
+ )
+ }
+ factory {
+ InfoBannerInteractionIntent()
+ }
+ factory {
+ KeyboardStateChangeIntent()
+ }
+ factory {
+ SkontoActiveChangeIntent(
+ getSkontoDiscountPercentageUseCase = get()
+ )
+ }
+ factory {
+ SkontoAmountFieldChangeIntent(
+ skontoAmountValidator = get(),
getSkontoDiscountPercentageUseCase = get(),
- getSkontoEdgeCaseUseCase = get(),
getSkontoSavedAmountUseCase = get(),
+ )
+ }
+ factory {
+ SkontoDueDateChangeIntent(
getSkontoRemainingDaysUseCase = get(),
- getSkontoDefaultSelectionStateUseCase = get(),
- skontoExtractionsHandler = get(),
- lastAnalyzedDocumentProvider = get(),
- skontoInvoicePreviewTextLinesFactory = get(),
- lastExtractionsProvider = get(),
+ getSkontoEdgeCaseUseCase = get(),
+ )
+ }
+ factory {
+ TransactionDocDialogDecisionIntent(
+ openExtractionsScreenSubIntent = get(),
transactionDocDialogConfirmAttachUseCase = get(),
transactionDocDialogCancelAttachUseCase = get(),
- getTransactionDocShouldBeAutoAttachedUseCase = get(),
- getTransactionDocsFeatureEnabledUseCase = get(),
)
}
factory {
- SkontoInvoicePreviewTextLinesFactory(
- resources = androidContext().resources,
- amountFormatter = get()
+ OpenExtractionsScreenSubIntent(
+ skontoExtractionsHandler = get(),
+ lastExtractionsProvider = get()
)
}
}
\ No newline at end of file
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoScreenSideEffect.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoScreenSideEffect.kt
new file mode 100644
index 000000000..9d4556bfb
--- /dev/null
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoScreenSideEffect.kt
@@ -0,0 +1,8 @@
+package net.gini.android.bank.sdk.capture.skonto
+
+internal sealed interface SkontoScreenSideEffect {
+ data class OpenInvoiceScreen(
+ val documentId: String,
+ val infoTextLines: List,
+ ) : SkontoScreenSideEffect
+}
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoScreenState.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoScreenState.kt
new file mode 100644
index 000000000..6af359a4c
--- /dev/null
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoScreenState.kt
@@ -0,0 +1,37 @@
+package net.gini.android.bank.sdk.capture.skonto
+
+import net.gini.android.bank.sdk.capture.skonto.model.SkontoData
+import net.gini.android.bank.sdk.capture.skonto.model.SkontoEdgeCase
+import net.gini.android.capture.Amount
+import java.math.BigDecimal
+import java.time.LocalDate
+
+internal sealed interface SkontoScreenState {
+
+ data class Ready(
+ val isSkontoSectionActive: Boolean,
+ val paymentInDays: Int,
+ val skontoPercentage: BigDecimal,
+ val skontoAmount: Amount,
+ val skontoAmountValidationError: SkontoAmountValidationError?,
+ val discountDueDate: LocalDate,
+ val fullAmount: Amount,
+ val fullAmountValidationError: FullAmountValidationError?,
+ val totalAmount: Amount,
+ val savedAmount: Amount,
+ val paymentMethod: SkontoData.SkontoPaymentMethod,
+ val edgeCase: SkontoEdgeCase?,
+ val edgeCaseInfoDialogVisible: Boolean,
+ val transactionDialogVisible: Boolean,
+ ) : SkontoScreenState {
+
+ sealed interface SkontoAmountValidationError {
+ object SkontoAmountMoreThanFullAmount : SkontoAmountValidationError
+ object SkontoAmountLimitExceeded : SkontoAmountValidationError
+ }
+
+ sealed interface FullAmountValidationError {
+ object FullAmountLimitExceeded : FullAmountValidationError
+ }
+ }
+}
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/mapper/FullAmountValidationErrorMapper.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/mapper/FullAmountValidationErrorMapper.kt
new file mode 100644
index 000000000..7914b9901
--- /dev/null
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/mapper/FullAmountValidationErrorMapper.kt
@@ -0,0 +1,22 @@
+package net.gini.android.bank.sdk.capture.skonto.mapper
+
+import android.content.res.Resources
+import net.gini.android.bank.sdk.R
+import net.gini.android.bank.sdk.capture.skonto.SkontoScreenState
+import net.gini.android.bank.sdk.capture.skonto.formatter.AmountFormatter
+import net.gini.android.bank.sdk.capture.skonto.validation.SkontoAmountValidator
+import net.gini.android.capture.Amount
+
+private val maxAmount =
+ Amount.parse("${SkontoAmountValidator.SKONTO_AMOUNT_LIMIT}:EUR")
+
+internal fun SkontoScreenState.Ready.FullAmountValidationError.toErrorMessage(
+ resources: Resources,
+ amountFormatter: AmountFormatter,
+): String = when (this) {
+ is SkontoScreenState.Ready.FullAmountValidationError.FullAmountLimitExceeded ->
+ resources.getString(
+ R.string.gbs_skonto_section_without_discount_field_amount_validation_error_limit_exceeded,
+ amountFormatter.format(maxAmount)
+ )
+}
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/mapper/SkontoAmountValidationErrorMapper.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/mapper/SkontoAmountValidationErrorMapper.kt
new file mode 100644
index 000000000..3decb5231
--- /dev/null
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/mapper/SkontoAmountValidationErrorMapper.kt
@@ -0,0 +1,27 @@
+package net.gini.android.bank.sdk.capture.skonto.mapper
+
+import android.content.res.Resources
+import net.gini.android.bank.sdk.R
+import net.gini.android.bank.sdk.capture.skonto.SkontoScreenState
+import net.gini.android.bank.sdk.capture.skonto.formatter.AmountFormatter
+import net.gini.android.bank.sdk.capture.skonto.validation.SkontoAmountValidator
+import net.gini.android.capture.Amount
+
+private val maxAmount =
+ Amount.parse("${SkontoAmountValidator.SKONTO_AMOUNT_LIMIT}:EUR")
+
+internal fun SkontoScreenState.Ready.SkontoAmountValidationError.toErrorMessage(
+ resources: Resources,
+ amountFormatter: AmountFormatter,
+): String = when (this) {
+ is SkontoScreenState.Ready.SkontoAmountValidationError.SkontoAmountMoreThanFullAmount ->
+ resources.getString(
+ R.string.gbs_skonto_section_discount_field_amount_validation_error_skonto_amount_more_than_full_amount
+ )
+
+ SkontoScreenState.Ready.SkontoAmountValidationError.SkontoAmountLimitExceeded ->
+ resources.getString(
+ R.string.gbs_skonto_section_discount_field_amount_validation_error_limit_exceeded,
+ amountFormatter.format(maxAmount)
+ )
+}
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/validation/SkontoAmountValidator.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/validation/SkontoAmountValidator.kt
new file mode 100644
index 000000000..6b91f6e30
--- /dev/null
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/validation/SkontoAmountValidator.kt
@@ -0,0 +1,24 @@
+package net.gini.android.bank.sdk.capture.skonto.validation
+
+import net.gini.android.bank.sdk.capture.skonto.SkontoScreenState
+import java.math.BigDecimal
+
+internal class SkontoAmountValidator {
+
+ fun execute(
+ newSkontoAmount: BigDecimal,
+ fullAmount: BigDecimal
+ ): SkontoScreenState.Ready.SkontoAmountValidationError? = when {
+ newSkontoAmount > fullAmount ->
+ SkontoScreenState.Ready.SkontoAmountValidationError.SkontoAmountMoreThanFullAmount
+
+ newSkontoAmount > BigDecimal(SKONTO_AMOUNT_LIMIT) ->
+ SkontoScreenState.Ready.SkontoAmountValidationError.SkontoAmountLimitExceeded
+
+ else -> null
+ }
+
+ companion object {
+ internal const val SKONTO_AMOUNT_LIMIT = "99999.99"
+ }
+}
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/validation/SkontoFullAmountValidator.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/validation/SkontoFullAmountValidator.kt
new file mode 100644
index 000000000..832844a62
--- /dev/null
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/validation/SkontoFullAmountValidator.kt
@@ -0,0 +1,21 @@
+package net.gini.android.bank.sdk.capture.skonto.validation
+
+import net.gini.android.bank.sdk.capture.skonto.SkontoScreenState
+import java.math.BigDecimal
+
+internal class SkontoFullAmountValidator {
+
+ fun execute(
+ fullAmount: BigDecimal
+ ): SkontoScreenState.Ready.FullAmountValidationError? = when {
+
+ fullAmount > BigDecimal(SKONTO_AMOUNT_LIMIT) ->
+ SkontoScreenState.Ready.FullAmountValidationError.FullAmountLimitExceeded
+
+ else -> null
+ }
+
+ companion object {
+ private const val SKONTO_AMOUNT_LIMIT = "99999.99"
+ }
+}
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/SkontoFragmentViewModel.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/SkontoFragmentViewModel.kt
new file mode 100644
index 000000000..9aa5cada2
--- /dev/null
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/SkontoFragmentViewModel.kt
@@ -0,0 +1,82 @@
+package net.gini.android.bank.sdk.capture.skonto.viewmodel
+
+import androidx.lifecycle.ViewModel
+import net.gini.android.bank.sdk.capture.skonto.SkontoFragmentListener
+import net.gini.android.bank.sdk.capture.skonto.SkontoScreenSideEffect
+import net.gini.android.bank.sdk.capture.skonto.SkontoScreenState
+import net.gini.android.bank.sdk.capture.skonto.model.SkontoData
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.intent.FullAmountChangeIntent
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.intent.InfoBannerInteractionIntent
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.intent.InvoiceClickIntent
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.intent.KeyboardStateChangeIntent
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.intent.ProceedClickedIntent
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.intent.SkontoActiveChangeIntent
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.intent.SkontoAmountFieldChangeIntent
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.intent.SkontoDueDateChangeIntent
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.intent.TransactionDocDialogDecisionIntent
+import org.orbitmvi.orbit.Container
+import org.orbitmvi.orbit.ContainerHost
+import org.orbitmvi.orbit.viewmodel.container
+import java.math.BigDecimal
+import java.time.LocalDate
+
+internal typealias SkontoScreenContainerHost = ContainerHost
+
+internal class SkontoFragmentViewModel(
+ data: SkontoData,
+ skontoScreenInitialStateFactory: SkontoScreenInitialStateFactory,
+
+ private val proceedClickedIntent: ProceedClickedIntent,
+ private val skontoActiveChangeIntent: SkontoActiveChangeIntent,
+ private val keyboardStateChangeIntent: KeyboardStateChangeIntent,
+ private val skontoAmountFieldChangeIntent: SkontoAmountFieldChangeIntent,
+ private val invoiceClickIntent: InvoiceClickIntent,
+ private val fullAmountChangeIntent: FullAmountChangeIntent,
+ private val skontoDueDateChangeIntent: SkontoDueDateChangeIntent,
+ private val transactionDocDialogDecisionIntent: TransactionDocDialogDecisionIntent,
+ private val infoBannerInteractionIntent: InfoBannerInteractionIntent,
+) : ViewModel(), SkontoScreenContainerHost {
+
+ override val container: Container = container(
+ skontoScreenInitialStateFactory.create(data)
+ )
+
+ private var listener: SkontoFragmentListener? = null
+
+ fun setListener(listener: SkontoFragmentListener?) {
+ this.listener = listener
+ }
+
+ fun onProceedClicked() =
+ with(proceedClickedIntent) { run(listener) }
+
+ fun onConfirmAttachTransactionDocClicked(alwaysAttach: Boolean) =
+ with(transactionDocDialogDecisionIntent) { runConfirm(alwaysAttach, listener) }
+
+ fun onCancelAttachTransactionDocClicked() =
+ with(transactionDocDialogDecisionIntent) { runCancel(listener) }
+
+ fun onSkontoActiveChanged(newValue: Boolean) =
+ with(skontoActiveChangeIntent) { run(newValue) }
+
+ fun onKeyboardStateChanged(isVisible: Boolean) =
+ with(keyboardStateChangeIntent) { run(isVisible) }
+
+ fun onSkontoAmountFieldChanged(newValue: BigDecimal) =
+ with(skontoAmountFieldChangeIntent) { run(newValue) }
+
+ fun onSkontoDueDateChanged(newDate: LocalDate) =
+ with(skontoDueDateChangeIntent) { run(newDate) }
+
+ fun onFullAmountFieldChanged(newValue: BigDecimal) =
+ with(fullAmountChangeIntent) { run(newValue) }
+
+ fun onInfoBannerClicked() =
+ with(infoBannerInteractionIntent) { runClick() }
+
+ fun onInfoDialogDismissed() =
+ with(infoBannerInteractionIntent) { runDismiss() }
+
+ fun onInvoiceClicked() =
+ with(invoiceClickIntent) { run() }
+}
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/SkontoScreenInitialStateFactory.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/SkontoScreenInitialStateFactory.kt
new file mode 100644
index 000000000..3ff3d682e
--- /dev/null
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/SkontoScreenInitialStateFactory.kt
@@ -0,0 +1,51 @@
+package net.gini.android.bank.sdk.capture.skonto.viewmodel
+
+import net.gini.android.bank.sdk.capture.skonto.SkontoScreenState
+import net.gini.android.bank.sdk.capture.skonto.model.SkontoData
+import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoDefaultSelectionStateUseCase
+import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoEdgeCaseUseCase
+import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoSavedAmountUseCase
+import net.gini.android.capture.Amount
+
+internal class SkontoScreenInitialStateFactory(
+ private val getSkontoSavedAmountUseCase: GetSkontoSavedAmountUseCase,
+ private val getSkontoEdgeCaseUseCase: GetSkontoEdgeCaseUseCase,
+ private val getSkontoDefaultSelectionStateUseCase: GetSkontoDefaultSelectionStateUseCase,
+) {
+
+ fun create(data: SkontoData): SkontoScreenState.Ready {
+
+ val discount = data.skontoPercentageDiscounted
+
+ val paymentMethod = data.skontoPaymentMethod ?: SkontoData.SkontoPaymentMethod.Unspecified
+ val edgeCase = getSkontoEdgeCaseUseCase.execute(data.skontoDueDate, paymentMethod)
+
+ val isSkontoSectionActive = getSkontoDefaultSelectionStateUseCase.execute(edgeCase)
+
+ val totalAmount =
+ if (isSkontoSectionActive) data.skontoAmountToPay else data.fullAmountToPay
+
+ val savedAmountValue = getSkontoSavedAmountUseCase.execute(
+ data.skontoAmountToPay.value,
+ data.fullAmountToPay.value
+ )
+ val savedAmount = Amount(savedAmountValue, data.fullAmountToPay.currency)
+
+ return SkontoScreenState.Ready(
+ isSkontoSectionActive = isSkontoSectionActive,
+ paymentInDays = data.skontoRemainingDays,
+ skontoPercentage = discount,
+ skontoAmount = data.skontoAmountToPay,
+ discountDueDate = data.skontoDueDate,
+ fullAmount = data.fullAmountToPay,
+ totalAmount = totalAmount,
+ paymentMethod = paymentMethod,
+ edgeCase = edgeCase,
+ edgeCaseInfoDialogVisible = edgeCase != null,
+ savedAmount = savedAmount,
+ transactionDialogVisible = false,
+ skontoAmountValidationError = null,
+ fullAmountValidationError = null,
+ )
+ }
+}
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/FullAmountChangeIntent.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/FullAmountChangeIntent.kt
new file mode 100644
index 000000000..046f1f86b
--- /dev/null
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/FullAmountChangeIntent.kt
@@ -0,0 +1,57 @@
+package net.gini.android.bank.sdk.capture.skonto.viewmodel.intent
+
+import net.gini.android.bank.sdk.capture.skonto.SkontoScreenState
+import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoAmountUseCase
+import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoSavedAmountUseCase
+import net.gini.android.bank.sdk.capture.skonto.validation.SkontoFullAmountValidator
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.SkontoFragmentViewModel
+import net.gini.android.capture.Amount
+import java.math.BigDecimal
+
+internal class FullAmountChangeIntent(
+ private val skontoFullAmountValidator: SkontoFullAmountValidator,
+ private val getSkontoAmountUseCase: GetSkontoAmountUseCase,
+ private val getSkontoSavedAmountUseCase: GetSkontoSavedAmountUseCase,
+) {
+
+ fun SkontoFragmentViewModel.run(newValue: BigDecimal) = intent {
+ val state = state as? SkontoScreenState.Ready ?: return@intent
+
+ if (newValue == state.fullAmount.value) return@intent
+
+ val validationError = skontoFullAmountValidator.execute(newValue)
+
+ if (validationError != null) {
+ reduce {
+ state.copy(
+ fullAmountValidationError = validationError
+ )
+ }
+ return@intent
+ }
+
+ val totalAmount =
+ if (state.isSkontoSectionActive) state.skontoAmount.value else newValue
+
+ val discount = state.skontoPercentage
+
+ val skontoAmount = getSkontoAmountUseCase.execute(newValue, discount)
+
+ val savedAmountValue = getSkontoSavedAmountUseCase.execute(
+ skontoAmount,
+ newValue
+ )
+
+ val savedAmount = Amount(savedAmountValue, state.fullAmount.currency)
+
+ reduce {
+ state.copy(
+ fullAmountValidationError = validationError,
+ skontoAmount = state.skontoAmount.copy(value = skontoAmount),
+ fullAmount = state.fullAmount.copy(value = newValue),
+ totalAmount = state.totalAmount.copy(value = totalAmount),
+ savedAmount = savedAmount,
+ )
+ }
+ }
+}
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/InfoBannerInteractionIntent.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/InfoBannerInteractionIntent.kt
new file mode 100644
index 000000000..f0961fbb4
--- /dev/null
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/InfoBannerInteractionIntent.kt
@@ -0,0 +1,25 @@
+package net.gini.android.bank.sdk.capture.skonto.viewmodel.intent
+
+import net.gini.android.bank.sdk.capture.skonto.SkontoScreenState
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.SkontoScreenContainerHost
+
+internal class InfoBannerInteractionIntent {
+
+ fun SkontoScreenContainerHost.runClick() = intent {
+ val state = state as? SkontoScreenState.Ready ?: return@intent
+ reduce {
+ state.copy(
+ edgeCaseInfoDialogVisible = true,
+ )
+ }
+ }
+
+ fun SkontoScreenContainerHost.runDismiss() = intent {
+ val state = state as? SkontoScreenState.Ready ?: return@intent
+ reduce {
+ state.copy(
+ edgeCaseInfoDialogVisible = false,
+ )
+ }
+ }
+}
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/InvoiceClickIntent.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/InvoiceClickIntent.kt
new file mode 100644
index 000000000..ac4a827a0
--- /dev/null
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/InvoiceClickIntent.kt
@@ -0,0 +1,35 @@
+package net.gini.android.bank.sdk.capture.skonto.viewmodel.intent
+
+import net.gini.android.bank.sdk.capture.skonto.SkontoScreenSideEffect
+import net.gini.android.bank.sdk.capture.skonto.SkontoScreenState
+import net.gini.android.bank.sdk.capture.skonto.factory.lines.SkontoInvoicePreviewTextLinesFactory
+import net.gini.android.bank.sdk.capture.skonto.model.SkontoData
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.SkontoScreenContainerHost
+import net.gini.android.capture.analysis.LastAnalyzedDocumentProvider
+
+internal class InvoiceClickIntent(
+ private val lastAnalyzedDocumentProvider: LastAnalyzedDocumentProvider,
+ private val skontoInvoicePreviewTextLinesFactory: SkontoInvoicePreviewTextLinesFactory
+) {
+
+ fun SkontoScreenContainerHost.run() = intent {
+ val state = state as? SkontoScreenState.Ready ?: return@intent
+
+ val skontoData = SkontoData(
+ skontoAmountToPay = state.skontoAmount,
+ skontoDueDate = state.discountDueDate,
+ skontoPercentageDiscounted = state.skontoPercentage,
+ skontoRemainingDays = state.paymentInDays,
+ fullAmountToPay = state.fullAmount,
+ skontoPaymentMethod = state.paymentMethod,
+ )
+ val documentId = lastAnalyzedDocumentProvider.provide()?.giniApiDocumentId ?: return@intent
+
+ postSideEffect(
+ SkontoScreenSideEffect.OpenInvoiceScreen(
+ documentId,
+ skontoInvoicePreviewTextLinesFactory.create(skontoData)
+ )
+ )
+ }
+}
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/KeyboardStateChangeIntent.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/KeyboardStateChangeIntent.kt
new file mode 100644
index 000000000..9ac4f58cf
--- /dev/null
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/KeyboardStateChangeIntent.kt
@@ -0,0 +1,18 @@
+package net.gini.android.bank.sdk.capture.skonto.viewmodel.intent
+
+import net.gini.android.bank.sdk.capture.skonto.SkontoScreenState
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.SkontoScreenContainerHost
+
+internal class KeyboardStateChangeIntent {
+
+ fun SkontoScreenContainerHost.run(visible: Boolean) = intent {
+ if (visible) return@intent
+ val state = state as? SkontoScreenState.Ready ?: return@intent
+ reduce {
+ state.copy(
+ fullAmountValidationError = null,
+ skontoAmountValidationError = null
+ )
+ }
+ }
+}
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/ProceedClickedIntent.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/ProceedClickedIntent.kt
new file mode 100644
index 000000000..91ee56a27
--- /dev/null
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/ProceedClickedIntent.kt
@@ -0,0 +1,36 @@
+package net.gini.android.bank.sdk.capture.skonto.viewmodel.intent
+
+import net.gini.android.bank.sdk.capture.skonto.SkontoFragmentListener
+import net.gini.android.bank.sdk.capture.skonto.SkontoScreenState
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.SkontoScreenContainerHost
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.subintent.OpenExtractionsScreenSubIntent
+import net.gini.android.bank.sdk.transactiondocs.internal.usecase.GetTransactionDocShouldBeAutoAttachedUseCase
+import net.gini.android.bank.sdk.transactiondocs.internal.usecase.GetTransactionDocsFeatureEnabledUseCase
+import net.gini.android.bank.sdk.transactiondocs.internal.usecase.TransactionDocDialogConfirmAttachUseCase
+
+internal class ProceedClickedIntent(
+ private val openExtractionsScreenSubIntent: OpenExtractionsScreenSubIntent,
+ private val getTransactionDocShouldBeAutoAttachedUseCase: GetTransactionDocShouldBeAutoAttachedUseCase,
+ private val getTransactionDocsFeatureEnabledUseCase: GetTransactionDocsFeatureEnabledUseCase,
+ private val transactionDocDialogConfirmAttachUseCase: TransactionDocDialogConfirmAttachUseCase,
+) {
+
+ fun SkontoScreenContainerHost.run(skontoFragmentListener: SkontoFragmentListener?) = intent {
+ val state = state as? SkontoScreenState.Ready ?: return@intent
+
+ if (!getTransactionDocsFeatureEnabledUseCase()) {
+ with(openExtractionsScreenSubIntent) {
+ run(skontoFragmentListener)
+ }
+ return@intent
+ }
+ if (getTransactionDocShouldBeAutoAttachedUseCase()) {
+ transactionDocDialogConfirmAttachUseCase(true)
+ with(openExtractionsScreenSubIntent) {
+ run(skontoFragmentListener)
+ }
+ } else {
+ reduce { state.copy(transactionDialogVisible = true) }
+ }
+ }
+}
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/SkontoActiveChangeIntent.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/SkontoActiveChangeIntent.kt
new file mode 100644
index 000000000..f6d4ac80e
--- /dev/null
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/SkontoActiveChangeIntent.kt
@@ -0,0 +1,27 @@
+package net.gini.android.bank.sdk.capture.skonto.viewmodel.intent
+
+import net.gini.android.bank.sdk.capture.skonto.SkontoScreenState
+import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoDiscountPercentageUseCase
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.SkontoScreenContainerHost
+
+internal class SkontoActiveChangeIntent(
+ private val getSkontoDiscountPercentageUseCase: GetSkontoDiscountPercentageUseCase,
+) {
+
+ fun SkontoScreenContainerHost.run(newValue: Boolean) = intent {
+ val state = state as? SkontoScreenState.Ready ?: return@intent
+ val totalAmount = if (newValue) state.skontoAmount else state.fullAmount
+ val discount = getSkontoDiscountPercentageUseCase.execute(
+ state.skontoAmount.value,
+ state.fullAmount.value
+ )
+
+ reduce {
+ state.copy(
+ isSkontoSectionActive = newValue,
+ totalAmount = totalAmount,
+ skontoPercentage = discount
+ )
+ }
+ }
+}
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/SkontoAmountFieldChangeIntent.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/SkontoAmountFieldChangeIntent.kt
new file mode 100644
index 000000000..5b2e3dfc1
--- /dev/null
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/SkontoAmountFieldChangeIntent.kt
@@ -0,0 +1,67 @@
+package net.gini.android.bank.sdk.capture.skonto.viewmodel.intent
+
+import net.gini.android.bank.sdk.capture.skonto.SkontoScreenState
+import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoDiscountPercentageUseCase
+import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoSavedAmountUseCase
+import net.gini.android.bank.sdk.capture.skonto.validation.SkontoAmountValidator
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.SkontoScreenContainerHost
+import net.gini.android.capture.Amount
+import java.math.BigDecimal
+
+internal class SkontoAmountFieldChangeIntent(
+ private val skontoAmountValidator: SkontoAmountValidator,
+ private val getSkontoDiscountPercentageUseCase: GetSkontoDiscountPercentageUseCase,
+ private val getSkontoSavedAmountUseCase: GetSkontoSavedAmountUseCase,
+) {
+
+ fun SkontoScreenContainerHost.run(newValue: BigDecimal) = intent {
+ val state = state as? SkontoScreenState.Ready ?: return@intent
+
+ if (newValue == state.skontoAmount.value) return@intent
+
+ val skontoAmountValidationError = skontoAmountValidator.execute(
+ newValue,
+ state.fullAmount.value
+ )
+
+ if (skontoAmountValidationError != null) {
+ reduce {
+ state.copy(
+ skontoAmount = state.skontoAmount,
+ skontoAmountValidationError = SkontoScreenState
+ .Ready.SkontoAmountValidationError.SkontoAmountMoreThanFullAmount
+ )
+ }
+ return@intent
+ }
+
+ val discount = getSkontoDiscountPercentageUseCase.execute(
+ newValue,
+ state.fullAmount.value
+ )
+
+ val totalAmount = if (state.isSkontoSectionActive)
+ newValue
+ else state.fullAmount.value
+
+ val newSkontoAmount = state.skontoAmount.copy(value = newValue)
+ val newTotalAmount = state.totalAmount.copy(value = totalAmount)
+
+ val savedAmountValue = getSkontoSavedAmountUseCase.execute(
+ newSkontoAmount.value,
+ state.fullAmount.value
+ )
+
+ val savedAmount = Amount(savedAmountValue, state.fullAmount.currency)
+
+ reduce {
+ state.copy(
+ skontoAmountValidationError = skontoAmountValidationError,
+ skontoAmount = newSkontoAmount,
+ skontoPercentage = discount,
+ totalAmount = newTotalAmount,
+ savedAmount = savedAmount,
+ )
+ }
+ }
+}
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/SkontoDueDateChangeIntent.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/SkontoDueDateChangeIntent.kt
new file mode 100644
index 000000000..d3de22a7a
--- /dev/null
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/SkontoDueDateChangeIntent.kt
@@ -0,0 +1,28 @@
+package net.gini.android.bank.sdk.capture.skonto.viewmodel.intent
+
+import net.gini.android.bank.sdk.capture.skonto.SkontoScreenState
+import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoEdgeCaseUseCase
+import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoRemainingDaysUseCase
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.SkontoScreenContainerHost
+import java.time.LocalDate
+
+internal class SkontoDueDateChangeIntent(
+ private val getSkontoRemainingDaysUseCase: GetSkontoRemainingDaysUseCase,
+ private val getSkontoEdgeCaseUseCase: GetSkontoEdgeCaseUseCase,
+) {
+
+ fun SkontoScreenContainerHost.run(newDate: LocalDate) = intent {
+ val state = state as? SkontoScreenState.Ready ?: return@intent
+ val newPayInDays = getSkontoRemainingDaysUseCase.execute(newDate)
+ reduce {
+ state.copy(
+ discountDueDate = newDate,
+ paymentInDays = newPayInDays,
+ edgeCase = getSkontoEdgeCaseUseCase.execute(
+ dueDate = newDate,
+ paymentMethod = state.paymentMethod
+ )
+ )
+ }
+ }
+}
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/TransactionDocDialogDecisionIntent.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/TransactionDocDialogDecisionIntent.kt
new file mode 100644
index 000000000..24ac6cd8d
--- /dev/null
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/TransactionDocDialogDecisionIntent.kt
@@ -0,0 +1,27 @@
+package net.gini.android.bank.sdk.capture.skonto.viewmodel.intent
+
+import net.gini.android.bank.sdk.capture.skonto.SkontoFragmentListener
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.SkontoScreenContainerHost
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.subintent.OpenExtractionsScreenSubIntent
+import net.gini.android.bank.sdk.transactiondocs.internal.usecase.TransactionDocDialogCancelAttachUseCase
+import net.gini.android.bank.sdk.transactiondocs.internal.usecase.TransactionDocDialogConfirmAttachUseCase
+
+internal class TransactionDocDialogDecisionIntent(
+ private val openExtractionsScreenSubIntent: OpenExtractionsScreenSubIntent,
+ private val transactionDocDialogConfirmAttachUseCase: TransactionDocDialogConfirmAttachUseCase,
+ private val transactionDocDialogCancelAttachUseCase: TransactionDocDialogCancelAttachUseCase,
+) {
+
+ fun SkontoScreenContainerHost.runConfirm(
+ alwaysAttach: Boolean,
+ listener: SkontoFragmentListener?
+ ) = intent {
+ transactionDocDialogConfirmAttachUseCase(alwaysAttach)
+ with(openExtractionsScreenSubIntent) { run(listener) }
+ }
+
+ fun SkontoScreenContainerHost.runCancel(listener: SkontoFragmentListener?) = intent {
+ transactionDocDialogCancelAttachUseCase()
+ with(openExtractionsScreenSubIntent) { run(listener) }
+ }
+}
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/subintent/OpenExtractionsScreenSubIntent.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/subintent/OpenExtractionsScreenSubIntent.kt
new file mode 100644
index 000000000..9f6803543
--- /dev/null
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/subintent/OpenExtractionsScreenSubIntent.kt
@@ -0,0 +1,32 @@
+@file:OptIn(OrbitExperimental::class)
+
+package net.gini.android.bank.sdk.capture.skonto.viewmodel.subintent
+
+import net.gini.android.bank.sdk.capture.extractions.skonto.SkontoExtractionsHandler
+import net.gini.android.bank.sdk.capture.skonto.SkontoFragmentListener
+import net.gini.android.bank.sdk.capture.skonto.SkontoScreenState
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.SkontoScreenContainerHost
+import net.gini.android.capture.provider.LastExtractionsProvider
+import org.orbitmvi.orbit.annotation.OrbitExperimental
+
+internal class OpenExtractionsScreenSubIntent(
+ private val skontoExtractionsHandler: SkontoExtractionsHandler,
+ private val lastExtractionsProvider: LastExtractionsProvider,
+) {
+
+ suspend fun SkontoScreenContainerHost.run(listener: SkontoFragmentListener?) = subIntent {
+ val state = state as? SkontoScreenState.Ready ?: return@subIntent
+ skontoExtractionsHandler.updateExtractions(
+ totalAmount = state.totalAmount,
+ skontoPercentage = state.skontoPercentage,
+ skontoAmount = state.skontoAmount,
+ paymentInDays = state.paymentInDays,
+ discountDueDate = state.discountDueDate.toString(),
+ )
+ lastExtractionsProvider.update(skontoExtractionsHandler.getExtractions().toMutableMap())
+ listener?.onPayInvoiceWithSkonto(
+ skontoExtractionsHandler.getExtractions(),
+ skontoExtractionsHandler.getCompoundExtractions()
+ )
+ }
+}
diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/util/ui/KeyboardExt.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/util/ui/KeyboardExt.kt
new file mode 100644
index 000000000..91f749ddc
--- /dev/null
+++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/util/ui/KeyboardExt.kt
@@ -0,0 +1,34 @@
+package net.gini.android.bank.sdk.util.ui
+
+import android.view.ViewTreeObserver
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.platform.LocalWindowInfo
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+
+@Composable
+fun keyboardAsState(): State {
+ val view = LocalView.current
+ var isImeVisible by remember { mutableStateOf(false) }
+
+ DisposableEffect(LocalWindowInfo.current) {
+ val listener = ViewTreeObserver.OnPreDrawListener {
+ isImeVisible = ViewCompat.getRootWindowInsets(view)
+ ?.isVisible(WindowInsetsCompat.Type.ime()) == true
+ true
+ }
+ view.viewTreeObserver.addOnPreDrawListener(listener)
+ onDispose {
+ view.viewTreeObserver.removeOnPreDrawListener(listener)
+ }
+ }
+ return rememberUpdatedState(isImeVisible)
+}
diff --git a/bank-sdk/sdk/src/main/res/values-en/strings.xml b/bank-sdk/sdk/src/main/res/values-en/strings.xml
index d22442ee4..9d98552bb 100644
--- a/bank-sdk/sdk/src/main/res/values-en/strings.xml
+++ b/bank-sdk/sdk/src/main/res/values-en/strings.xml
@@ -59,11 +59,14 @@
The %1$s discount has expired
Final amount
Expiry date
+ Your transfer limit has been exceeded: %1$s
+ Discounted value cannot exceed initial value
Without Skonto discount
• Active
Full amount
+ Your transfer limit has been exceeded: %1$s
Total
diff --git a/bank-sdk/sdk/src/main/res/values/strings.xml b/bank-sdk/sdk/src/main/res/values/strings.xml
index 6aa4fc862..95df80db1 100644
--- a/bank-sdk/sdk/src/main/res/values/strings.xml
+++ b/bank-sdk/sdk/src/main/res/values/strings.xml
@@ -59,11 +59,14 @@
Die %1$s Skonto sind abgelaufen
Betrag nach Abzug
Ablaufdatum Skonto
+ Ihr Überweisungslimit wurde überschritten: %1$s
+ Der Betrag mit Skonto darf nicht höher sein als ohne
Ohne Skonto
• Aktiviert
Betrag ohne Abzug
+ Ihr Überweisungslimit wurde überschritten: %1$s
Gesamtpreis
diff --git a/bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragmentViewModelTest.kt b/bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragmentViewModelTest.kt
index 616ac9bb5..d25b3099e 100644
--- a/bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragmentViewModelTest.kt
+++ b/bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragmentViewModelTest.kt
@@ -1,13 +1,11 @@
package net.gini.android.bank.sdk.capture.skonto
-import app.cash.turbine.test
import io.mockk.Runs
import io.mockk.coVerify
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.verify
-import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import net.gini.android.bank.sdk.MainDispatcherRule
import net.gini.android.bank.sdk.capture.skonto.factory.lines.SkontoInvoicePreviewTextLinesFactory
@@ -16,11 +14,25 @@ import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoAmountUseCase
import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoDefaultSelectionStateUseCase
import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoDiscountPercentageUseCase
import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoEdgeCaseUseCase
+import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoRemainingDaysUseCase
import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoSavedAmountUseCase
+import net.gini.android.bank.sdk.capture.skonto.validation.SkontoAmountValidator
+import net.gini.android.bank.sdk.capture.skonto.validation.SkontoFullAmountValidator
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.SkontoFragmentViewModel
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.SkontoScreenInitialStateFactory
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.intent.FullAmountChangeIntent
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.intent.InfoBannerInteractionIntent
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.intent.InvoiceClickIntent
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.intent.ProceedClickedIntent
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.intent.SkontoActiveChangeIntent
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.intent.SkontoAmountFieldChangeIntent
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.intent.SkontoDueDateChangeIntent
+import net.gini.android.bank.sdk.capture.skonto.viewmodel.subintent.OpenExtractionsScreenSubIntent
import net.gini.android.capture.Amount
import net.gini.android.capture.analysis.LastAnalyzedDocumentProvider
import org.junit.Rule
import org.junit.Test
+import org.orbitmvi.orbit.test.test
import java.math.BigDecimal
import java.time.LocalDate
@@ -40,26 +52,31 @@ class SkontoFragmentViewModelTest {
val getSkontoDefaultSelectionStateUseCase: GetSkontoDefaultSelectionStateUseCase =
mockk(relaxed = true)
- val viewModel = SkontoFragmentViewModel(
- data = skontoData,
- getTransactionDocsFeatureEnabledUseCase = mockk(),
- getSkontoDiscountPercentageUseCase = mockk(),
+ val skontoScreenInitialStateFactory = SkontoScreenInitialStateFactory(
getSkontoSavedAmountUseCase = getSkontoSavedAmountUseCase,
getSkontoEdgeCaseUseCase = getSkontoEdgeCaseUseCase,
- getSkontoAmountUseCase = mockk(),
- getSkontoRemainingDaysUseCase = mockk(),
getSkontoDefaultSelectionStateUseCase = getSkontoDefaultSelectionStateUseCase,
- skontoExtractionsHandler = mockk(),
- lastAnalyzedDocumentProvider = mockk(),
- skontoInvoicePreviewTextLinesFactory = mockk(),
- lastExtractionsProvider = mockk(),
- transactionDocDialogConfirmAttachUseCase = mockk(),
- transactionDocDialogCancelAttachUseCase = mockk(),
- getTransactionDocShouldBeAutoAttachedUseCase = mockk(),
)
- val flowData = viewModel.stateFlow.first()
- assert(flowData is SkontoFragmentContract.State.Ready)
+ val viewModel = SkontoFragmentViewModel(
+ data = skontoData,
+
+ skontoScreenInitialStateFactory = skontoScreenInitialStateFactory,
+
+ proceedClickedIntent = mockk(),
+ skontoActiveChangeIntent = mockk(),
+ keyboardStateChangeIntent = mockk(),
+ skontoAmountFieldChangeIntent = mockk(),
+ invoiceClickIntent = mockk(),
+ fullAmountChangeIntent = mockk(),
+ skontoDueDateChangeIntent = mockk(),
+ transactionDocDialogDecisionIntent = mockk(),
+ infoBannerInteractionIntent = mockk(),
+ )
+
+ viewModel.test(this) {
+ expectInitialState()
+ }
}
@Test
@@ -74,26 +91,32 @@ class SkontoFragmentViewModelTest {
val getSkontoDefaultSelectionStateUseCase: GetSkontoDefaultSelectionStateUseCase =
mockk(relaxed = true)
- val viewModel = SkontoFragmentViewModel(
- data = skontoData,
- getTransactionDocsFeatureEnabledUseCase = mockk(),
- getSkontoDiscountPercentageUseCase = mockk(),
+ val skontoScreenInitialStateFactory = SkontoScreenInitialStateFactory(
getSkontoSavedAmountUseCase = getSkontoSavedAmountUseCase,
getSkontoEdgeCaseUseCase = getSkontoEdgeCaseUseCase,
- getSkontoAmountUseCase = mockk(),
- getSkontoRemainingDaysUseCase = mockk(),
getSkontoDefaultSelectionStateUseCase = getSkontoDefaultSelectionStateUseCase,
- skontoExtractionsHandler = mockk(),
- lastAnalyzedDocumentProvider = mockk(),
- skontoInvoicePreviewTextLinesFactory = mockk(),
- lastExtractionsProvider = mockk(),
- transactionDocDialogConfirmAttachUseCase = mockk(),
- transactionDocDialogCancelAttachUseCase = mockk(),
- getTransactionDocShouldBeAutoAttachedUseCase = mockk(),
)
- val flowData = viewModel.stateFlow.first()
- assert(flowData is SkontoFragmentContract.State.Ready)
+
+ val viewModel = SkontoFragmentViewModel(
+ data = skontoData,
+
+ skontoScreenInitialStateFactory = skontoScreenInitialStateFactory,
+
+ proceedClickedIntent = mockk(),
+ skontoActiveChangeIntent = mockk(),
+ keyboardStateChangeIntent = mockk(),
+ skontoAmountFieldChangeIntent = mockk(),
+ invoiceClickIntent = mockk(),
+ fullAmountChangeIntent = mockk(),
+ skontoDueDateChangeIntent = mockk(),
+ transactionDocDialogDecisionIntent = mockk(),
+ infoBannerInteractionIntent = mockk(),
+ )
+
+ viewModel.test(this) {
+ expectInitialState()
+ }
coVerify(exactly = 1) {
getSkontoSavedAmountUseCase.execute(any(), any())
@@ -108,35 +131,41 @@ class SkontoFragmentViewModelTest {
runTest {
val skontoData: SkontoData = mockk(relaxed = true)
- val viewModel = SkontoFragmentViewModel(
- data = skontoData,
- getTransactionDocsFeatureEnabledUseCase = mockk(),
- getSkontoDiscountPercentageUseCase = mockk(),
- getSkontoSavedAmountUseCase = mockk(relaxed = true),
- getSkontoEdgeCaseUseCase = mockk(relaxed = true),
- getSkontoAmountUseCase = mockk(),
- getSkontoRemainingDaysUseCase = mockk(),
- getSkontoDefaultSelectionStateUseCase = mockk(relaxed = true),
- skontoExtractionsHandler = mockk(),
- lastAnalyzedDocumentProvider = mockk(),
- skontoInvoicePreviewTextLinesFactory = mockk(),
- lastExtractionsProvider = mockk(),
- transactionDocDialogConfirmAttachUseCase = mockk(),
- transactionDocDialogCancelAttachUseCase = mockk(),
- getTransactionDocShouldBeAutoAttachedUseCase = mockk(),
+ val getSkontoSavedAmountUseCase: GetSkontoSavedAmountUseCase =
+ mockk(relaxed = true)
+ val getSkontoEdgeCaseUseCase: GetSkontoEdgeCaseUseCase =
+ mockk(relaxed = true)
+ val getSkontoDefaultSelectionStateUseCase: GetSkontoDefaultSelectionStateUseCase =
+ mockk(relaxed = true)
+
+ val skontoScreenInitialStateFactory = SkontoScreenInitialStateFactory(
+ getSkontoSavedAmountUseCase = getSkontoSavedAmountUseCase,
+ getSkontoEdgeCaseUseCase = getSkontoEdgeCaseUseCase,
+ getSkontoDefaultSelectionStateUseCase = getSkontoDefaultSelectionStateUseCase,
)
- with(viewModel.stateFlow.first()) {
- assert(this is SkontoFragmentContract.State.Ready)
- require(this is SkontoFragmentContract.State.Ready)
- }
+ val infoBannerInteractionIntent = InfoBannerInteractionIntent()
- viewModel.onInfoBannerClicked()
+ val viewModel = SkontoFragmentViewModel(
+ data = skontoData,
+ skontoScreenInitialStateFactory = skontoScreenInitialStateFactory,
+ proceedClickedIntent = mockk(),
+ skontoActiveChangeIntent = mockk(),
+ keyboardStateChangeIntent = mockk(),
+ skontoAmountFieldChangeIntent = mockk(),
+ invoiceClickIntent = mockk(),
+ fullAmountChangeIntent = mockk(),
+ skontoDueDateChangeIntent = mockk(),
+ transactionDocDialogDecisionIntent = mockk(),
+ infoBannerInteractionIntent = infoBannerInteractionIntent,
+ )
- with(viewModel.stateFlow.first()) {
- assert(this is SkontoFragmentContract.State.Ready)
- require(this is SkontoFragmentContract.State.Ready)
- assert(this.edgeCaseInfoDialogVisible)
+ viewModel.test(this) {
+ runOnCreate()
+ containerHost.onInfoBannerClicked()
+ expectState {
+ (this as SkontoScreenState.Ready).copy(edgeCaseInfoDialogVisible = true)
+ }
}
}
@@ -145,33 +174,42 @@ class SkontoFragmentViewModelTest {
runTest {
val skontoData: SkontoData = mockk(relaxed = true)
- val viewModel = SkontoFragmentViewModel(
- data = skontoData,
- getTransactionDocsFeatureEnabledUseCase = mockk(),
- getSkontoDiscountPercentageUseCase = mockk(),
- getSkontoSavedAmountUseCase = mockk(relaxed = true),
- getSkontoEdgeCaseUseCase = mockk(relaxed = true),
- getSkontoAmountUseCase = mockk(),
- getSkontoRemainingDaysUseCase = mockk(),
- getSkontoDefaultSelectionStateUseCase = mockk(relaxed = true),
- skontoExtractionsHandler = mockk(),
- lastAnalyzedDocumentProvider = mockk(),
- skontoInvoicePreviewTextLinesFactory = mockk(),
- lastExtractionsProvider = mockk(),
- transactionDocDialogConfirmAttachUseCase = mockk(),
- transactionDocDialogCancelAttachUseCase = mockk(),
- getTransactionDocShouldBeAutoAttachedUseCase = mockk(),
+ val getSkontoSavedAmountUseCase: GetSkontoSavedAmountUseCase =
+ mockk(relaxed = true)
+ val getSkontoEdgeCaseUseCase: GetSkontoEdgeCaseUseCase =
+ mockk(relaxed = true)
+ val getSkontoDefaultSelectionStateUseCase: GetSkontoDefaultSelectionStateUseCase =
+ mockk(relaxed = true)
+
+ val skontoScreenInitialStateFactory = SkontoScreenInitialStateFactory(
+ getSkontoSavedAmountUseCase = getSkontoSavedAmountUseCase,
+ getSkontoEdgeCaseUseCase = getSkontoEdgeCaseUseCase,
+ getSkontoDefaultSelectionStateUseCase = getSkontoDefaultSelectionStateUseCase,
)
- viewModel.stateFlow.value = mockk(relaxed = true)
- .copy(edgeCaseInfoDialogVisible = true)
+ val infoBannerInteractionIntent = InfoBannerInteractionIntent()
- viewModel.onInfoDialogDismissed()
+ val viewModel = SkontoFragmentViewModel(
+ data = skontoData,
+ skontoScreenInitialStateFactory = skontoScreenInitialStateFactory,
+ proceedClickedIntent = mockk(),
+ skontoActiveChangeIntent = mockk(),
+ keyboardStateChangeIntent = mockk(),
+ skontoAmountFieldChangeIntent = mockk(),
+ invoiceClickIntent = mockk(),
+ fullAmountChangeIntent = mockk(),
+ skontoDueDateChangeIntent = mockk(),
+ transactionDocDialogDecisionIntent = mockk(),
+ infoBannerInteractionIntent = infoBannerInteractionIntent,
+ )
- with(viewModel.stateFlow.first()) {
- assert(this is SkontoFragmentContract.State.Ready)
- require(this is SkontoFragmentContract.State.Ready)
- assert(!this.edgeCaseInfoDialogVisible)
+ viewModel.test(this) {
+ runOnCreate()
+ expectInitialState()
+ containerHost.onInfoDialogDismissed()
+ expectState {
+ (this as SkontoScreenState.Ready).copy(edgeCaseInfoDialogVisible = false)
+ }
}
}
@@ -179,102 +217,112 @@ class SkontoFragmentViewModelTest {
fun `when user clicks on invoice preview the VM should fire the navigation side effect`() =
runTest {
val skontoData: SkontoData = mockk(relaxed = true)
+
+
val lastAnalyzedDocumentProvider = mockk {
every { provide() } returns mockk(relaxed = true)
}
val skontoInvoicePreviewTextLinesFactory = mockk {
every { create(any()) } returns mockk(relaxed = true)
}
- val viewModel = SkontoFragmentViewModel(
- data = skontoData,
- getTransactionDocsFeatureEnabledUseCase = mockk(),
- getSkontoDiscountPercentageUseCase = mockk(),
- getSkontoSavedAmountUseCase = mockk(relaxed = true),
- getSkontoEdgeCaseUseCase = mockk(relaxed = true),
- getSkontoAmountUseCase = mockk(),
- getSkontoRemainingDaysUseCase = mockk(),
- getSkontoDefaultSelectionStateUseCase = mockk(relaxed = true),
- skontoExtractionsHandler = mockk(),
+
+ val getSkontoSavedAmountUseCase: GetSkontoSavedAmountUseCase =
+ mockk(relaxed = true)
+ val getSkontoEdgeCaseUseCase: GetSkontoEdgeCaseUseCase =
+ mockk(relaxed = true)
+ val getSkontoDefaultSelectionStateUseCase: GetSkontoDefaultSelectionStateUseCase =
+ mockk(relaxed = true)
+
+ val invoiceClickIntent = InvoiceClickIntent(
lastAnalyzedDocumentProvider = lastAnalyzedDocumentProvider,
skontoInvoicePreviewTextLinesFactory = skontoInvoicePreviewTextLinesFactory,
- lastExtractionsProvider = mockk(),
- transactionDocDialogConfirmAttachUseCase = mockk(),
- transactionDocDialogCancelAttachUseCase = mockk(),
- getTransactionDocShouldBeAutoAttachedUseCase = mockk(),
)
- viewModel.sideEffectFlow.test {
- viewModel.onInvoiceClicked()
- assert(awaitItem() is SkontoFragmentContract.SideEffect.OpenInvoiceScreen)
- }
- }
-
- @Test
- fun `when user disables skonto the final amount should be changed to full price`() =
- runTest {
- val skontoData: SkontoData = mockk(relaxed = true)
+ val skontoScreenInitialStateFactory = SkontoScreenInitialStateFactory(
+ getSkontoSavedAmountUseCase = getSkontoSavedAmountUseCase,
+ getSkontoEdgeCaseUseCase = getSkontoEdgeCaseUseCase,
+ getSkontoDefaultSelectionStateUseCase = getSkontoDefaultSelectionStateUseCase,
+ )
val viewModel = SkontoFragmentViewModel(
data = skontoData,
- getTransactionDocsFeatureEnabledUseCase = mockk(),
- getSkontoDiscountPercentageUseCase = mockk(relaxed = true),
- getSkontoSavedAmountUseCase = mockk(relaxed = true),
- getSkontoEdgeCaseUseCase = mockk(relaxed = true),
- getSkontoAmountUseCase = mockk(),
- getSkontoRemainingDaysUseCase = mockk(),
- getSkontoDefaultSelectionStateUseCase = mockk(relaxed = true),
- skontoExtractionsHandler = mockk(),
- lastAnalyzedDocumentProvider = mockk(),
- skontoInvoicePreviewTextLinesFactory = mockk(),
- lastExtractionsProvider = mockk(),
- transactionDocDialogConfirmAttachUseCase = mockk(),
- transactionDocDialogCancelAttachUseCase = mockk(),
- getTransactionDocShouldBeAutoAttachedUseCase = mockk(),
+ skontoScreenInitialStateFactory = skontoScreenInitialStateFactory,
+ proceedClickedIntent = mockk(),
+ skontoActiveChangeIntent = mockk(),
+ keyboardStateChangeIntent = mockk(),
+ skontoAmountFieldChangeIntent = mockk(),
+ invoiceClickIntent = invoiceClickIntent,
+ fullAmountChangeIntent = mockk(),
+ skontoDueDateChangeIntent = mockk(),
+ transactionDocDialogDecisionIntent = mockk(),
+ infoBannerInteractionIntent = mockk(),
)
- viewModel.stateFlow.test {
- skipItems(1) // skip initial state
- viewModel.onSkontoActiveChanged(false)
- with(awaitItem()) {
- assert(this is SkontoFragmentContract.State.Ready)
- require(this is SkontoFragmentContract.State.Ready)
- assert(!this.isSkontoSectionActive)
- assert(this.totalAmount == this.fullAmount)
- }
+ viewModel.test(this) {
+ expectInitialState()
+ containerHost.onInvoiceClicked()
+ assert(awaitSideEffect() is SkontoScreenSideEffect.OpenInvoiceScreen)
}
}
@Test
- fun `when user enables skonto the final amount should be changed to skonto price`() =
+ fun `when user swith skonto the final amount should be changed`() =
runTest {
val skontoData: SkontoData = mockk(relaxed = true)
+ val getSkontoSavedAmountUseCase: GetSkontoSavedAmountUseCase =
+ mockk(relaxed = true)
+ val getSkontoEdgeCaseUseCase: GetSkontoEdgeCaseUseCase =
+ mockk(relaxed = true)
+ val getSkontoDefaultSelectionStateUseCase: GetSkontoDefaultSelectionStateUseCase =
+ mockk(relaxed = true)
+
+ val skontoScreenInitialStateFactory = SkontoScreenInitialStateFactory(
+ getSkontoSavedAmountUseCase = getSkontoSavedAmountUseCase,
+ getSkontoEdgeCaseUseCase = getSkontoEdgeCaseUseCase,
+ getSkontoDefaultSelectionStateUseCase = getSkontoDefaultSelectionStateUseCase,
+ )
+
+ val getSkontoDiscountPercentageUseCase: GetSkontoDiscountPercentageUseCase = mockk() {
+ every { execute(any(), any()) } returns BigDecimal.ZERO
+ }
+
+ val skontoActiveChangeIntent = SkontoActiveChangeIntent(
+ getSkontoDiscountPercentageUseCase = getSkontoDiscountPercentageUseCase,
+ )
+
val viewModel = SkontoFragmentViewModel(
data = skontoData,
- getTransactionDocsFeatureEnabledUseCase = mockk(),
- getSkontoDiscountPercentageUseCase = mockk(relaxed = true),
- getSkontoSavedAmountUseCase = mockk(relaxed = true),
- getSkontoEdgeCaseUseCase = mockk(relaxed = true),
- getSkontoAmountUseCase = mockk(),
- getSkontoRemainingDaysUseCase = mockk(),
- getSkontoDefaultSelectionStateUseCase = mockk(relaxed = true),
- skontoExtractionsHandler = mockk(),
- lastAnalyzedDocumentProvider = mockk(),
- skontoInvoicePreviewTextLinesFactory = mockk(),
- lastExtractionsProvider = mockk(),
- transactionDocDialogConfirmAttachUseCase = mockk(),
- transactionDocDialogCancelAttachUseCase = mockk(),
- getTransactionDocShouldBeAutoAttachedUseCase = mockk(),
+ skontoScreenInitialStateFactory = skontoScreenInitialStateFactory,
+ proceedClickedIntent = mockk(),
+ skontoActiveChangeIntent = skontoActiveChangeIntent,
+ keyboardStateChangeIntent = mockk(),
+ skontoAmountFieldChangeIntent = mockk(),
+ invoiceClickIntent = mockk(),
+ fullAmountChangeIntent = mockk(),
+ skontoDueDateChangeIntent = mockk(),
+ transactionDocDialogDecisionIntent = mockk(),
+ infoBannerInteractionIntent = mockk(),
)
- viewModel.stateFlow.test {
- skipItems(1) // skip initial state
+ viewModel.test(this) {
+ expectInitialState()
+ runOnCreate()
+ viewModel.onSkontoActiveChanged(false)
+ expectState {
+ (this as SkontoScreenState.Ready).copy(
+ isSkontoSectionActive = false,
+ totalAmount = fullAmount,
+ skontoPercentage = BigDecimal.ZERO
+ )
+ }
viewModel.onSkontoActiveChanged(true)
- with(awaitItem()) {
- assert(this is SkontoFragmentContract.State.Ready)
- require(this is SkontoFragmentContract.State.Ready)
- assert(this.isSkontoSectionActive)
- assert(this.totalAmount == this.skontoAmount)
+ expectState {
+ (this as SkontoScreenState.Ready).copy(
+ isSkontoSectionActive = true,
+ totalAmount = skontoAmount,
+ skontoPercentage = BigDecimal.ZERO
+ )
}
}
}
@@ -284,76 +332,118 @@ class SkontoFragmentViewModelTest {
runTest {
val skontoData: SkontoData = mockk(relaxed = true) {
every { fullAmountToPay } returns Amount.parse("100:EUR")
+ every { skontoAmountToPay } returns Amount.parse("90:EUR")
+ every { skontoPercentageDiscounted } returns BigDecimal.ZERO
}
- val getSkontoDiscountPercentageUseCase = mockk(
- relaxed = true
- ) {
- every { execute(any(), any()) } returns mockk()
+ val getSkontoSavedAmountUseCase: GetSkontoSavedAmountUseCase = mockk {
+ every { execute(any(), any()) } returns BigDecimal.ZERO
}
+ val getSkontoEdgeCaseUseCase: GetSkontoEdgeCaseUseCase =
+ mockk(relaxed = true)
+
+ val getSkontoDefaultSelectionStateUseCase: GetSkontoDefaultSelectionStateUseCase =
+ mockk { every { execute(any()) } returns true }
+
+ val skontoScreenInitialStateFactory = SkontoScreenInitialStateFactory(
+ getSkontoSavedAmountUseCase = getSkontoSavedAmountUseCase,
+ getSkontoEdgeCaseUseCase = getSkontoEdgeCaseUseCase,
+ getSkontoDefaultSelectionStateUseCase = getSkontoDefaultSelectionStateUseCase,
+ )
+
+ val getSkontoDiscountPercentageUseCase: GetSkontoDiscountPercentageUseCase = mockk {
+ every { execute(any(), any()) } returns BigDecimal.ZERO
+ }
+
+ val skontoAmountFieldChangeIntent = SkontoAmountFieldChangeIntent(
+ skontoAmountValidator = SkontoAmountValidator(),
+ getSkontoDiscountPercentageUseCase = getSkontoDiscountPercentageUseCase,
+ getSkontoSavedAmountUseCase = getSkontoSavedAmountUseCase,
+ )
+
val viewModel = SkontoFragmentViewModel(
data = skontoData,
- getTransactionDocsFeatureEnabledUseCase = mockk(),
- getSkontoDiscountPercentageUseCase = getSkontoDiscountPercentageUseCase,
- getSkontoSavedAmountUseCase = mockk(relaxed = true),
- getSkontoEdgeCaseUseCase = mockk(relaxed = true),
- getSkontoAmountUseCase = mockk(),
- getSkontoRemainingDaysUseCase = mockk(),
- getSkontoDefaultSelectionStateUseCase = mockk(relaxed = true),
- skontoExtractionsHandler = mockk(),
- lastAnalyzedDocumentProvider = mockk(),
- skontoInvoicePreviewTextLinesFactory = mockk(),
- lastExtractionsProvider = mockk(),
- transactionDocDialogConfirmAttachUseCase = mockk(),
- transactionDocDialogCancelAttachUseCase = mockk(),
- getTransactionDocShouldBeAutoAttachedUseCase = mockk(),
+ skontoScreenInitialStateFactory = skontoScreenInitialStateFactory,
+ proceedClickedIntent = mockk(),
+ skontoActiveChangeIntent = mockk(),
+ keyboardStateChangeIntent = mockk(),
+ skontoAmountFieldChangeIntent = skontoAmountFieldChangeIntent,
+ invoiceClickIntent = mockk(),
+ fullAmountChangeIntent = mockk(),
+ skontoDueDateChangeIntent = mockk(),
+ transactionDocDialogDecisionIntent = mockk(),
+ infoBannerInteractionIntent = mockk(),
)
- viewModel.onSkontoAmountFieldChanged(BigDecimal("95"))
+ viewModel.test(this) {
+ expectInitialState()
+ runOnCreate()
+ val newSkontoAmount = BigDecimal("95")
+ containerHost.onSkontoAmountFieldChanged(newSkontoAmount)
+ expectState {
+ (this as SkontoScreenState.Ready).copy(
+ skontoAmount = skontoAmount.copy(value = newSkontoAmount),
+ totalAmount = totalAmount.copy(value = newSkontoAmount),
+ )
+ }
+ }
coVerify(exactly = 1) {
- getSkontoDiscountPercentageUseCase.execute(
- any(), any()
- )
+ getSkontoDiscountPercentageUseCase.execute(any(), any())
}
}
@Test
- fun `when user changes skonto amount to incorrect value no action should be performed`() =
+ fun `when user changes skonto amount to incorrect value error should be shown`() =
runTest {
val skontoData: SkontoData = mockk(relaxed = true) {
every { fullAmountToPay } returns Amount.parse("100:EUR")
+ every { skontoAmountToPay } returns Amount.parse("90:EUR")
}
val getSkontoDiscountPercentageUseCase = mockk {
every { execute(any(), any()) } returns mockk()
}
- val viewModel = SkontoFragmentViewModel(
- data = skontoData,
- getTransactionDocsFeatureEnabledUseCase = mockk(),
- getSkontoDiscountPercentageUseCase = getSkontoDiscountPercentageUseCase,
+ val skontoScreenInitialStateFactory = SkontoScreenInitialStateFactory(
getSkontoSavedAmountUseCase = mockk(relaxed = true),
getSkontoEdgeCaseUseCase = mockk(relaxed = true),
- getSkontoAmountUseCase = mockk(),
- getSkontoRemainingDaysUseCase = mockk(),
getSkontoDefaultSelectionStateUseCase = mockk(relaxed = true),
- skontoExtractionsHandler = mockk(),
- lastAnalyzedDocumentProvider = mockk(),
- skontoInvoicePreviewTextLinesFactory = mockk(),
- lastExtractionsProvider = mockk(),
- transactionDocDialogConfirmAttachUseCase = mockk(),
- transactionDocDialogCancelAttachUseCase = mockk(),
- getTransactionDocShouldBeAutoAttachedUseCase = mockk(),
)
- viewModel.onSkontoAmountFieldChanged(BigDecimal("110"))
+ val skontoAmountFieldChangeIntent = SkontoAmountFieldChangeIntent(
+ skontoAmountValidator = SkontoAmountValidator(),
+ getSkontoDiscountPercentageUseCase = getSkontoDiscountPercentageUseCase,
+ getSkontoSavedAmountUseCase = mockk(),
+ )
+
+ val viewModel = SkontoFragmentViewModel(
+ data = skontoData,
+ skontoScreenInitialStateFactory = skontoScreenInitialStateFactory,
+ proceedClickedIntent = mockk(),
+ skontoActiveChangeIntent = mockk(),
+ keyboardStateChangeIntent = mockk(),
+ skontoAmountFieldChangeIntent = skontoAmountFieldChangeIntent,
+ invoiceClickIntent = mockk(),
+ fullAmountChangeIntent = mockk(),
+ skontoDueDateChangeIntent = mockk(),
+ transactionDocDialogDecisionIntent = mockk(),
+ infoBannerInteractionIntent = mockk(),
+ )
- coVerify(exactly = 0) {
- getSkontoDiscountPercentageUseCase.execute(
- any(), any()
- )
+ viewModel.test(this) {
+ expectInitialState()
+ runOnCreate()
+ containerHost.onSkontoAmountFieldChanged(BigDecimal("110"))
+ expectState {
+ with(this as SkontoScreenState.Ready) {
+ copy(
+ skontoAmountValidationError = SkontoScreenState
+ .Ready.SkontoAmountValidationError.SkontoAmountMoreThanFullAmount
+ )
+ }
+ }
}
}
@@ -361,35 +451,63 @@ class SkontoFragmentViewModelTest {
fun `when user changes full amount the skonto amount should be recalculated`() =
runTest {
val skontoData: SkontoData = mockk(relaxed = true) {
- every { skontoAmountToPay } returns Amount.parse("100:EUR")
- every { fullAmountToPay } returns Amount.parse("150:EUR")
+ every { fullAmountToPay } returns Amount.parse("100:EUR")
+ every { skontoAmountToPay } returns Amount.parse("90:EUR")
+ every { skontoPercentageDiscounted } returns BigDecimal.ZERO
}
- val getSkontoAmountUseCase = mockk {
- every { execute(any(), any()) } returns mockk()
+ val getSkontoSavedAmountUseCase: GetSkontoSavedAmountUseCase = mockk {
+ every { execute(any(), any()) } returns BigDecimal.ZERO
}
+ val getSkontoAmountUseCase: GetSkontoAmountUseCase = mockk {
+ every { execute(any(), any()) } returns BigDecimal.ONE
+ }
+
+ val getSkontoEdgeCaseUseCase: GetSkontoEdgeCaseUseCase =
+ mockk(relaxed = true)
+
+ val getSkontoDefaultSelectionStateUseCase: GetSkontoDefaultSelectionStateUseCase =
+ mockk { every { execute(any()) } returns true }
+
+ val skontoScreenInitialStateFactory = SkontoScreenInitialStateFactory(
+ getSkontoSavedAmountUseCase = getSkontoSavedAmountUseCase,
+ getSkontoEdgeCaseUseCase = getSkontoEdgeCaseUseCase,
+ getSkontoDefaultSelectionStateUseCase = getSkontoDefaultSelectionStateUseCase,
+ )
+
+ val fullAmountChangeIntent = FullAmountChangeIntent(
+ skontoFullAmountValidator = SkontoFullAmountValidator(),
+ getSkontoAmountUseCase = getSkontoAmountUseCase,
+ getSkontoSavedAmountUseCase = getSkontoSavedAmountUseCase,
+ )
+
val viewModel = SkontoFragmentViewModel(
data = skontoData,
- getTransactionDocsFeatureEnabledUseCase = mockk(),
- getSkontoDiscountPercentageUseCase = mockk(relaxed = true),
- getSkontoSavedAmountUseCase = mockk(relaxed = true),
- getSkontoEdgeCaseUseCase = mockk(relaxed = true),
- getSkontoAmountUseCase = getSkontoAmountUseCase,
- getSkontoRemainingDaysUseCase = mockk(),
- getSkontoDefaultSelectionStateUseCase = mockk(relaxed = true) {
- every { execute(any()) } returns false
- },
- skontoExtractionsHandler = mockk(),
- lastAnalyzedDocumentProvider = mockk(),
- skontoInvoicePreviewTextLinesFactory = mockk(),
- lastExtractionsProvider = mockk(),
- transactionDocDialogConfirmAttachUseCase = mockk(),
- transactionDocDialogCancelAttachUseCase = mockk(),
- getTransactionDocShouldBeAutoAttachedUseCase = mockk(),
+ skontoScreenInitialStateFactory = skontoScreenInitialStateFactory,
+ proceedClickedIntent = mockk(),
+ skontoActiveChangeIntent = mockk(),
+ keyboardStateChangeIntent = mockk(),
+ skontoAmountFieldChangeIntent = mockk(),
+ invoiceClickIntent = mockk(),
+ fullAmountChangeIntent = fullAmountChangeIntent,
+ skontoDueDateChangeIntent = mockk(),
+ transactionDocDialogDecisionIntent = mockk(),
+ infoBannerInteractionIntent = mockk(),
)
- viewModel.onFullAmountFieldChanged(BigDecimal("200"))
+ viewModel.test(this) {
+ expectInitialState()
+ runOnCreate()
+ val newFullAmount = BigDecimal("200")
+ containerHost.onFullAmountFieldChanged(newFullAmount)
+ expectState {
+ (this as SkontoScreenState.Ready).copy(
+ fullAmount = skontoAmount.copy(value = newFullAmount),
+ skontoAmount = skontoAmount.copy(value = BigDecimal.ONE)
+ )
+ }
+ }
coVerify(exactly = 1) {
getSkontoAmountUseCase.execute(any(), any())
@@ -399,39 +517,59 @@ class SkontoFragmentViewModelTest {
@Test
fun `when user clicks proceed the extraction screen should be opened`() =
runTest {
- val skontoData: SkontoData = mockk(relaxed = true)
- val getSkontoAmountUseCase = mockk {
- every { execute(any(), any()) } returns mockk()
- }
+ val skontoData: SkontoData = mockk(relaxed = true)
- val listener = mockk(relaxed = true){
+ val listener = mockk(relaxed = true) {
every { onPayInvoiceWithSkonto(any(), any()) } just Runs
}
- val viewModel = SkontoFragmentViewModel(
- data = skontoData,
- getTransactionDocsFeatureEnabledUseCase = mockk {
- every { this@mockk.invoke() } returns false
- },
- getSkontoDiscountPercentageUseCase = mockk(relaxed = true),
- getSkontoSavedAmountUseCase = mockk(relaxed = true),
- getSkontoEdgeCaseUseCase = mockk(relaxed = true),
- getSkontoAmountUseCase = getSkontoAmountUseCase,
- getSkontoRemainingDaysUseCase = mockk(),
- getSkontoDefaultSelectionStateUseCase = mockk(relaxed = true),
+ val getSkontoSavedAmountUseCase: GetSkontoSavedAmountUseCase =
+ mockk(relaxed = true)
+ val getSkontoEdgeCaseUseCase: GetSkontoEdgeCaseUseCase =
+ mockk(relaxed = true)
+ val getSkontoDefaultSelectionStateUseCase: GetSkontoDefaultSelectionStateUseCase =
+ mockk(relaxed = true)
+
+ val skontoScreenInitialStateFactory = SkontoScreenInitialStateFactory(
+ getSkontoSavedAmountUseCase = getSkontoSavedAmountUseCase,
+ getSkontoEdgeCaseUseCase = getSkontoEdgeCaseUseCase,
+ getSkontoDefaultSelectionStateUseCase = getSkontoDefaultSelectionStateUseCase,
+ )
+
+ val openExtractionsScreenSubIntent = OpenExtractionsScreenSubIntent(
skontoExtractionsHandler = mockk(relaxed = true),
- lastAnalyzedDocumentProvider = mockk(),
- skontoInvoicePreviewTextLinesFactory = mockk(),
lastExtractionsProvider = mockk(relaxed = true),
- transactionDocDialogConfirmAttachUseCase = mockk(),
- transactionDocDialogCancelAttachUseCase = mockk(),
- getTransactionDocShouldBeAutoAttachedUseCase = mockk(),
+ )
+
+ val proceedClickedIntent = ProceedClickedIntent(
+ openExtractionsScreenSubIntent = openExtractionsScreenSubIntent,
+ getTransactionDocShouldBeAutoAttachedUseCase = mockk(relaxed = true),
+ getTransactionDocsFeatureEnabledUseCase = mockk(relaxed = true),
+ transactionDocDialogConfirmAttachUseCase = mockk(relaxed = true),
+ )
+
+ val viewModel = SkontoFragmentViewModel(
+ data = skontoData,
+ skontoScreenInitialStateFactory = skontoScreenInitialStateFactory,
+ proceedClickedIntent = proceedClickedIntent,
+ skontoActiveChangeIntent = mockk(),
+ keyboardStateChangeIntent = mockk(),
+ skontoAmountFieldChangeIntent = mockk(),
+ invoiceClickIntent = mockk(),
+ fullAmountChangeIntent = mockk(),
+ skontoDueDateChangeIntent = mockk(),
+ transactionDocDialogDecisionIntent = mockk(),
+ infoBannerInteractionIntent = mockk(),
)
viewModel.setListener(listener)
- viewModel.onProceedClicked()
+ viewModel.test(this) {
+ runOnCreate()
+ expectInitialState()
+ containerHost.onProceedClicked()
+ }
verify(exactly = 1) {
listener.onPayInvoiceWithSkonto(any(), any())
@@ -441,43 +579,57 @@ class SkontoFragmentViewModelTest {
@Test
fun `when user changes due date to date it should be applied`() =
runTest {
- val skontoData: SkontoData = mockk(relaxed = true)
+ val skontoData: SkontoData = mockk(relaxed = true) {
+ every { skontoDueDate } returns LocalDate.now()
+ }
- val viewModel = SkontoFragmentViewModel(
- data = skontoData,
- getTransactionDocsFeatureEnabledUseCase = mockk {
- every { this@mockk.invoke() } returns false
- },
- getSkontoDiscountPercentageUseCase = mockk(relaxed = true),
+ val getSkontoEdgeCaseUseCase = mockk {
+ every { execute(any(), any()) } returns mockk(relaxed = true)
+ }
+
+ val skontoScreenInitialStateFactory = SkontoScreenInitialStateFactory(
getSkontoSavedAmountUseCase = mockk(relaxed = true),
- getSkontoEdgeCaseUseCase = mockk(relaxed = true),
- getSkontoAmountUseCase = mockk(relaxed = true),
- getSkontoRemainingDaysUseCase = mockk(relaxed = true),
+ getSkontoEdgeCaseUseCase = getSkontoEdgeCaseUseCase,
getSkontoDefaultSelectionStateUseCase = mockk(relaxed = true),
- skontoExtractionsHandler = mockk(relaxed = true),
- lastAnalyzedDocumentProvider = mockk(),
- skontoInvoicePreviewTextLinesFactory = mockk(),
- lastExtractionsProvider = mockk(relaxed = true),
- transactionDocDialogConfirmAttachUseCase = mockk(),
- transactionDocDialogCancelAttachUseCase = mockk(),
- getTransactionDocShouldBeAutoAttachedUseCase = mockk(),
)
- viewModel.stateFlow.test {
- skipItems(1) // skip initial state
- val futureDueDate = LocalDate.now().plusDays(5)
- viewModel.onSkontoDueDateChanged(futureDueDate)
- with(awaitItem()) {
- assert(this is SkontoFragmentContract.State.Ready)
- require(this is SkontoFragmentContract.State.Ready)
- assert(this.discountDueDate == futureDueDate)
+ val skontoDueDateChangeIntent = SkontoDueDateChangeIntent(
+ getSkontoRemainingDaysUseCase = GetSkontoRemainingDaysUseCase(),
+ getSkontoEdgeCaseUseCase = getSkontoEdgeCaseUseCase,
+ )
+
+ val viewModel = SkontoFragmentViewModel(
+ data = skontoData,
+ skontoScreenInitialStateFactory = skontoScreenInitialStateFactory,
+ proceedClickedIntent = mockk(),
+ skontoActiveChangeIntent = mockk(),
+ keyboardStateChangeIntent = mockk(),
+ skontoAmountFieldChangeIntent = mockk(),
+ invoiceClickIntent = mockk(),
+ fullAmountChangeIntent = mockk(),
+ skontoDueDateChangeIntent = skontoDueDateChangeIntent,
+ transactionDocDialogDecisionIntent = mockk(),
+ infoBannerInteractionIntent = mockk(),
+ )
+
+ viewModel.test(this) {
+ expectInitialState()
+ runOnCreate()
+ val newDueDate = LocalDate.now().plusDays(5)
+ containerHost.onSkontoDueDateChanged(newDueDate)
+ expectState {
+ (this as SkontoScreenState.Ready).copy(
+ discountDueDate = newDueDate,
+ paymentInDays = 5
+ )
}
val pastDueDate = LocalDate.now().minusDays(5)
- viewModel.onSkontoDueDateChanged(pastDueDate)
- with(awaitItem()) {
- assert(this is SkontoFragmentContract.State.Ready)
- require(this is SkontoFragmentContract.State.Ready)
- assert(this.discountDueDate == pastDueDate)
+ containerHost.onSkontoDueDateChanged(pastDueDate)
+ expectState {
+ (this as SkontoScreenState.Ready).copy(
+ discountDueDate = pastDueDate,
+ paymentInDays = 5 // always absolute value
+ )
}
}
}
diff --git a/bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/skonto/usecase/SkontoAmountValidatorTest.kt b/bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/skonto/usecase/SkontoAmountValidatorTest.kt
new file mode 100644
index 000000000..c583ac142
--- /dev/null
+++ b/bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/skonto/usecase/SkontoAmountValidatorTest.kt
@@ -0,0 +1,37 @@
+package net.gini.android.bank.sdk.capture.skonto.usecase
+
+import net.gini.android.bank.sdk.capture.skonto.SkontoScreenState
+import net.gini.android.bank.sdk.capture.skonto.validation.SkontoAmountValidator
+import org.junit.Test
+import java.math.BigDecimal
+
+
+class SkontoAmountValidatorTest {
+
+ @Test
+ fun `skonto amount validation error should be null if skonto amount is less than or equal to full amount`() {
+ val useCase = SkontoAmountValidator()
+ val skontoAmount = BigDecimal("100.00")
+ val fullAmount = BigDecimal("200.00")
+ val result = useCase.execute(skontoAmount, fullAmount)
+ assert(result == null)
+ }
+
+ @Test
+ fun `skonto amount validation error should be SKONTO_AMOUNT_MORE_THAN_FULL_AMOUNT if skonto amount is greater than full amount`() {
+ val useCase = SkontoAmountValidator()
+ val skontoAmount = BigDecimal("300.00")
+ val fullAmount = BigDecimal("200.00")
+ val result = useCase.execute(skontoAmount, fullAmount)
+ assert(result == SkontoScreenState.Ready.SkontoAmountValidationError.SkontoAmountMoreThanFullAmount)
+ }
+
+ @Test
+ fun `skonto amount validation error should be SKONTO_AMOUNT_LIMIT_EXCEEDED if skonto amount is greater than MAX_AMOUNT`() {
+ val useCase = SkontoAmountValidator()
+ val skontoAmount = BigDecimal("1000000.00")
+ val fullAmount = BigDecimal("1000000.00")
+ val result = useCase.execute(skontoAmount, fullAmount)
+ assert(result == SkontoScreenState.Ready.SkontoAmountValidationError.SkontoAmountLimitExceeded)
+ }
+}
\ No newline at end of file
diff --git a/bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/skonto/usecase/SkontoFullAmountValidatorTest.kt b/bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/skonto/usecase/SkontoFullAmountValidatorTest.kt
new file mode 100644
index 000000000..5ebd9b5d6
--- /dev/null
+++ b/bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/skonto/usecase/SkontoFullAmountValidatorTest.kt
@@ -0,0 +1,25 @@
+package net.gini.android.bank.sdk.capture.skonto.usecase
+
+import net.gini.android.bank.sdk.capture.skonto.SkontoScreenState
+import net.gini.android.bank.sdk.capture.skonto.validation.SkontoFullAmountValidator
+import org.junit.Test
+import java.math.BigDecimal
+
+class SkontoFullAmountValidatorTest {
+
+ @Test
+ fun `full amount validation error should be null if full amount is less than or equal to MAX_AMOUNT`() {
+ val useCase = SkontoFullAmountValidator()
+ val fullAmount = BigDecimal("100.00")
+ val result = useCase.execute(fullAmount)
+ assert(result == null)
+ }
+
+ @Test
+ fun `full amount validation error should be MAX_AMOUNT_EXCEEDED if full amount is greater than MAX_AMOUNT`() {
+ val useCase = SkontoFullAmountValidator()
+ val fullAmount = BigDecimal("1000000.00")
+ val result = useCase.execute(fullAmount)
+ assert(result == SkontoScreenState.Ready.FullAmountValidationError.FullAmountLimitExceeded)
+ }
+}
\ No newline at end of file
diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/ui/components/textinput/GiniTextInput.kt b/capture-sdk/sdk/src/main/java/net/gini/android/capture/ui/components/textinput/GiniTextInput.kt
index 23fa5d711..6f5868198 100644
--- a/capture-sdk/sdk/src/main/java/net/gini/android/capture/ui/components/textinput/GiniTextInput.kt
+++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/ui/components/textinput/GiniTextInput.kt
@@ -29,6 +29,7 @@ fun GiniTextInput(
colors: GiniTextInputColors = GiniTextInputColors.colors(),
visualTransformation: VisualTransformation = VisualTransformation.None,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+ supportingText: @Composable (() -> Unit)? = null,
) {
GiniTextInput(
modifier = modifier,
@@ -48,6 +49,7 @@ fun GiniTextInput(
trailingContent = trailingContent,
colors = colors,
visualTransformation = visualTransformation,
+ supportingText = supportingText,
)
}
@@ -65,6 +67,7 @@ fun GiniTextInput(
colors: GiniTextInputColors = GiniTextInputColors.colors(),
visualTransformation: VisualTransformation = VisualTransformation.None,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+ supportingText: @Composable (() -> Unit)? = null,
) {
TextField(
modifier = modifier,
@@ -102,7 +105,8 @@ fun GiniTextInput(
)
},
onValueChange = onValueChange,
- trailingIcon = trailingContent
+ trailingIcon = trailingContent,
+ supportingText = supportingText,
)
}
diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/ui/components/textinput/GiniTextInputColors.kt b/capture-sdk/sdk/src/main/java/net/gini/android/capture/ui/components/textinput/GiniTextInputColors.kt
index 40e84341e..6a197ea33 100644
--- a/capture-sdk/sdk/src/main/java/net/gini/android/capture/ui/components/textinput/GiniTextInputColors.kt
+++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/ui/components/textinput/GiniTextInputColors.kt
@@ -37,7 +37,7 @@ data class GiniTextInputColors(
textFocused: Color = GiniTheme.colorScheme.textField.text.focused,
textUnfocused: Color = GiniTheme.colorScheme.textField.text.unfocused,
textDisabled: Color = GiniTheme.colorScheme.textField.text.disabled,
- textError: Color = GiniTheme.colorScheme.textField.text.focused,
+ textError: Color = GiniTheme.colorScheme.textField.text.error,
indicatorFocused: Color = GiniTheme.colorScheme.textField.indicator.focused,
indicatorUnfocused: Color = GiniTheme.colorScheme.textField.indicator.unfocused,
indicatorDisabled: Color = GiniTheme.colorScheme.textField.indicator.disabled,
diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/ui/components/textinput/amount/DecimalFormatter.kt b/capture-sdk/sdk/src/main/java/net/gini/android/capture/ui/components/textinput/amount/DecimalFormatter.kt
index e4d17d5bc..98e85f161 100644
--- a/capture-sdk/sdk/src/main/java/net/gini/android/capture/ui/components/textinput/amount/DecimalFormatter.kt
+++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/ui/components/textinput/amount/DecimalFormatter.kt
@@ -5,7 +5,7 @@ import java.text.DecimalFormat
import java.text.NumberFormat
class DecimalFormatter(
- val numberFormat: NumberFormat = NumberFormat.getCurrencyInstance().apply {
+ private val numberFormat: NumberFormat = NumberFormat.getCurrencyInstance().apply {
(this as? DecimalFormat)?.apply {
decimalFormatSymbols = decimalFormatSymbols.apply {
currencySymbol = ""
@@ -14,14 +14,12 @@ class DecimalFormatter(
}
) {
- fun parseAmount(amount: BigDecimal) = numberFormat.format(amount).trim()
- .filter { it != '.' && it != ',' }
- .take(7)
+ fun parseAmount(amount: BigDecimal) = numberFormat.format(amount).trim()
+ .filter { NUMBER_CHARS.contains(it) }
.trimStart('0')
fun textToDigits(text: String): String = text.trim()
- .filter { it != '.' && it != ',' }
- .take(7)
+ .filter { NUMBER_CHARS.contains(it) }
.trimStart('0')
fun parseDigits(digits: String): BigDecimal =
@@ -34,4 +32,8 @@ class DecimalFormatter(
// Format to a currency string
return numberFormat.format(decimal).trim()
}
+
+ companion object {
+ private val NUMBER_CHARS = "0123456789".toCharArray()
+ }
}
\ No newline at end of file
diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/ui/components/textinput/amount/GiniAmountTextInput.kt b/capture-sdk/sdk/src/main/java/net/gini/android/capture/ui/components/textinput/amount/GiniAmountTextInput.kt
index 9d02eda5b..b3b286d6e 100644
--- a/capture-sdk/sdk/src/main/java/net/gini/android/capture/ui/components/textinput/amount/GiniAmountTextInput.kt
+++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/ui/components/textinput/amount/GiniAmountTextInput.kt
@@ -3,6 +3,7 @@ package net.gini.android.capture.ui.components.textinput.amount
import android.content.res.Configuration
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -30,11 +31,12 @@ fun GiniAmountTextInput(
isError: Boolean = false,
decimalFormatter: DecimalFormatter = DecimalFormatter(),
colors: GiniTextInputColors = GiniTextInputColors.colors(),
+ supportingText: String? = null,
) {
val parsedAmount = decimalFormatter.parseAmount(amount)
var text by remember { mutableStateOf(parsedAmount) }
-
+
text = parsedAmount
GiniTextInput(
@@ -48,8 +50,11 @@ fun GiniAmountTextInput(
),
label = label,
onValueChange = {
- text = decimalFormatter.textToDigits(it) // take only 7 digits
- onValueChange(decimalFormatter.parseDigits(text))
+ val newText = decimalFormatter.textToDigits(it) // take only 7 digits
+ if (newText != text) {
+ text = newText
+ onValueChange(decimalFormatter.parseDigits(text))
+ }
},
trailingContent = trailingContent,
colors = colors,
@@ -58,6 +63,15 @@ fun GiniAmountTextInput(
currencyCode = currencyCode,
isCurrencyCodeDisplay = !enabled,
),
+ supportingText = supportingText?.let {
+ {
+ Text(
+ text = supportingText,
+ color = colors.textError,
+ style = GiniTheme.typography.caption1,
+ )
+ }
+ }
)
}
@@ -66,6 +80,7 @@ fun GiniAmountTextInput(
@Composable
private fun GiniTextInputPreviewLight() {
GiniTextInputPreview()
+ GiniTextInputPreviewError()
}
@Preview(
@@ -75,6 +90,7 @@ private fun GiniTextInputPreviewLight() {
@Composable
private fun GiniTextInputPreviewDark() {
GiniTextInputPreview()
+ GiniTextInputPreviewError()
}
@Composable
@@ -90,3 +106,20 @@ private fun GiniTextInputPreview() {
)
}
}
+
+@Composable
+private fun GiniTextInputPreviewError() {
+ GiniTheme {
+ GiniAmountTextInput(
+ modifier = Modifier.padding(16.dp),
+ amount = BigDecimal("1234"),
+ label = "Label Text",
+ trailingContent = { },
+ currencyCode = "EUR",
+ onValueChange = {},
+ isError = true,
+ supportingText = "Error text"
+ )
+ }
+}
+
diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/util/compose/ImeListener.kt b/capture-sdk/sdk/src/main/java/net/gini/android/capture/util/compose/ImeListener.kt
new file mode 100644
index 000000000..434bf459f
--- /dev/null
+++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/util/compose/ImeListener.kt
@@ -0,0 +1,31 @@
+package net.gini.android.capture.util.compose
+
+import android.view.ViewTreeObserver
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalView
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+
+@Composable
+fun rememberImeState(): State {
+ val imeState = remember { mutableStateOf(false) }
+
+ val view = LocalView.current
+ DisposableEffect(view) {
+ val listener = ViewTreeObserver.OnGlobalLayoutListener {
+ val isKeyboardOpen = ViewCompat.getRootWindowInsets(view)
+ ?.isVisible(WindowInsetsCompat.Type.ime()) ?: true
+ imeState.value = isKeyboardOpen
+ }
+
+ view.viewTreeObserver.addOnGlobalLayoutListener(listener)
+ onDispose {
+ view.viewTreeObserver.removeOnGlobalLayoutListener(listener)
+ }
+ }
+ return imeState
+}
diff --git a/capture-sdk/sdk/src/main/java/net/gini/android/capture/util/compose/KeybaordPaddingProvider.kt b/capture-sdk/sdk/src/main/java/net/gini/android/capture/util/compose/KeybaordPaddingProvider.kt
new file mode 100644
index 000000000..59b4625b3
--- /dev/null
+++ b/capture-sdk/sdk/src/main/java/net/gini/android/capture/util/compose/KeybaordPaddingProvider.kt
@@ -0,0 +1,44 @@
+package net.gini.android.capture.util.compose
+
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.ScrollState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import kotlin.math.roundToInt
+
+/**
+ * Provides a padding based on keyboard state. If keyboard is opened the passed padding will be
+ * returned and 0 will be returned otherwise
+ *
+ * In case if scrollState is passed - it will be scrolled automatically to this padding
+ */
+@Composable
+fun keyboardPadding(
+ padding: Dp,
+ scrollState: ScrollState? = null
+): State {
+ val keybaordState by rememberImeState()
+
+ val keyboardPadding = remember { mutableStateOf(padding) }
+ val paddingPx = with(LocalDensity.current) { padding.toPx().roundToInt() }
+
+ LaunchedEffect(keybaordState, paddingPx) {
+ if (keybaordState) {
+ keyboardPadding.value = padding
+ scrollState?.let { scrollState ->
+ scrollState.animateScrollTo(scrollState.value + paddingPx, tween(300))
+ }
+ } else {
+ keyboardPadding.value = 0.dp
+ }
+ }
+
+ return keyboardPadding
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 7aaac4672..2414e05b1 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -24,6 +24,7 @@ compose-bom = "2024.06.00"
accompanist-themeAdapter = "1.1.1"
compose-activity = "1.9.0"
koin-bom = "3.5.6"
+orbitMvi = "9.0.0"
[libraries]
android-gradle = { module = "com.android.tools.build:gradle", version.ref = "android-gradle-plugin" }
@@ -124,4 +125,7 @@ koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin-bom" }
koin-core = { module = "io.insert-koin:koin-core" }
koin-android-compat = { module = "io.insert-koin:koin-android-compat" }
koin-android = { module = "io.insert-koin:koin-android" }
-koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose" }
\ No newline at end of file
+koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose" }
+orbitmvi-compose = { group = "org.orbit-mvi", name = "orbit-compose", version.ref = "orbitMvi" }
+orbitmvi-test = { group = "org.orbit-mvi", name = "orbit-test", version.ref = "orbitMvi" }
+orbitmvi-viewmodel = { group = "org.orbit-mvi", name = "orbit-viewmodel", version.ref = "orbitMvi" }
\ No newline at end of file