From 44f429366ec2d69b5e2713fef7ed7b467499dbff Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 5 Nov 2024 18:29:22 +0100 Subject: [PATCH 01/24] feature(bank-sdk): Skonto. Amounts validation message PP-763 --- .../capture/di/skonto/SkontoCommonModule.kt | 8 + .../bank/sdk/capture/skonto/SkontoFragment.kt | 1017 +--------------- .../capture/skonto/SkontoFragmentContract.kt | 34 - .../capture/skonto/SkontoFragmentViewModel.kt | 60 +- .../sdk/capture/skonto/SkontoScreenContent.kt | 1050 +++++++++++++++++ .../sdk/capture/skonto/SkontoScreenModule.kt | 2 + .../capture/skonto/SkontoScreenSideEffect.kt | 8 + .../sdk/capture/skonto/SkontoScreenState.kt | 37 + .../mapper/FullAmountValidationErrorMapper.kt | 22 + .../SkontoAmountValidationErrorMapper.kt | 27 + .../GetFullAmountValidationErrorUseCase.kt | 21 + .../GetSkontoAmountValidationErrorUseCase.kt | 24 + .../sdk/src/main/res/values-en/strings.xml | 3 + bank-sdk/sdk/src/main/res/values/strings.xml | 3 + .../ui/components/textinput/GiniTextInput.kt | 6 +- .../textinput/GiniTextInputColors.kt | 2 +- .../textinput/amount/DecimalFormatter.kt | 2 +- .../textinput/amount/GiniAmountTextInput.kt | 32 +- 18 files changed, 1291 insertions(+), 1067 deletions(-) delete mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragmentContract.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoScreenContent.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoScreenSideEffect.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoScreenState.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/mapper/FullAmountValidationErrorMapper.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/mapper/SkontoAmountValidationErrorMapper.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetFullAmountValidationErrorUseCase.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetSkontoAmountValidationErrorUseCase.kt diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/di/skonto/SkontoCommonModule.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/di/skonto/SkontoCommonModule.kt index d4dd7b994..8f8181f0e 100644 --- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/di/skonto/SkontoCommonModule.kt +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/di/skonto/SkontoCommonModule.kt @@ -7,6 +7,8 @@ import net.gini.android.bank.sdk.capture.skonto.factory.text.SkontoSavedAmountTe 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.formatter.SkontoRemainingDaysFormatter +import net.gini.android.bank.sdk.capture.skonto.usecase.GetFullAmountValidationErrorUseCase +import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoAmountValidationErrorUseCase import net.gini.android.bank.sdk.capture.util.currencyFormatterWithoutSymbol import org.koin.android.ext.koin.androidContext import org.koin.dsl.module @@ -45,4 +47,10 @@ val skontoCommonModule = module { factory { SkontoDataExtractor() } + factory { + GetSkontoAmountValidationErrorUseCase() + } + factory { + GetFullAmountValidationErrorUseCase() + } } 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 6bdc8bcc4..64269801a 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,114 +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.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight -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.graphics.vector.rememberVectorPainter 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.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.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() { @@ -117,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 @@ -157,7 +71,7 @@ class SkontoFragment : Fragment() { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { GiniTheme { - ScreenContent( + SkontoScreenContent( viewModel = viewModel, isBottomNavigationBarEnabled = isBottomNavigationBarEnabled, customBottomNavBarAdapter = customBottomNavBarAdapter, @@ -180,933 +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 NavigationActionHelp( - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - IconButton( - modifier = modifier - .width(24.dp) - .height(24.dp), - onClick = onClick - ) { - Icon( - painter = painterResource(R.drawable.gbs_help_question_icon), - contentDescription = null, - ) - } -} - -@Composable -private fun NavigationActionBack( - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - IconButton( - modifier = modifier - .width(24.dp) - .height(24.dp), - onClick = onClick - ) { - Icon( - painter = rememberVectorPainter(image = Icons.AutoMirrored.Default.ArrowBack), - contentDescription = null, - ) - } -} - -@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 = rememberVectorPainter(image = Icons.AutoMirrored.Default.KeyboardArrowRight), - 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 = true, - 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 = true, -) \ 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 index c5c90b439..d1100a1e0 100644 --- 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 @@ -8,7 +8,9 @@ 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.GetFullAmountValidationErrorUseCase import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoAmountUseCase +import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoAmountValidationErrorUseCase 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 @@ -40,12 +42,14 @@ internal class SkontoFragmentViewModel( private val transactionDocDialogCancelAttachUseCase: TransactionDocDialogCancelAttachUseCase, private val getTransactionDocShouldBeAutoAttachedUseCase: GetTransactionDocShouldBeAutoAttachedUseCase, private val getTransactionDocsFeatureEnabledUseCase: GetTransactionDocsFeatureEnabledUseCase, + private val getSkontoAmountValidationErrorUseCase: GetSkontoAmountValidationErrorUseCase, + private val getFullAmountValidationErrorUseCase: GetFullAmountValidationErrorUseCase, ) : ViewModel() { - val stateFlow: MutableStateFlow = + val stateFlow: MutableStateFlow = MutableStateFlow(createInitalState(data)) - val sideEffectFlow: MutableSharedFlow = MutableSharedFlow() + val sideEffectFlow: MutableSharedFlow = MutableSharedFlow() private var listener: SkontoFragmentListener? = null @@ -54,7 +58,7 @@ internal class SkontoFragmentViewModel( } fun onProceedClicked() = viewModelScope.launch { - val currentState = stateFlow.value as? SkontoFragmentContract.State.Ready ?: return@launch + val currentState = stateFlow.value as? SkontoScreenState.Ready ?: return@launch if (!getTransactionDocsFeatureEnabledUseCase()) { openExtractionsScreen() return@launch @@ -77,7 +81,7 @@ internal class SkontoFragmentViewModel( } private fun openExtractionsScreen() { - val currentState = stateFlow.value as? SkontoFragmentContract.State.Ready ?: return + val currentState = stateFlow.value as? SkontoScreenState.Ready ?: return skontoExtractionsHandler.updateExtractions( totalAmount = currentState.totalAmount, skontoPercentage = currentState.skontoPercentage, @@ -94,7 +98,7 @@ internal class SkontoFragmentViewModel( private fun createInitalState( data: SkontoData, - ): SkontoFragmentContract.State.Ready { + ): SkontoScreenState.Ready { val discount = data.skontoPercentageDiscounted @@ -112,7 +116,7 @@ internal class SkontoFragmentViewModel( ) val savedAmount = Amount(savedAmountValue, data.fullAmountToPay.currency) - return SkontoFragmentContract.State.Ready( + return SkontoScreenState.Ready( isSkontoSectionActive = isSkontoSectionActive, paymentInDays = data.skontoRemainingDays, skontoPercentage = discount, @@ -125,11 +129,13 @@ internal class SkontoFragmentViewModel( edgeCaseInfoDialogVisible = edgeCase != null, savedAmount = savedAmount, transactionDialogVisible = false, + skontoAmountValidationError = null, + fullAmountValidationError = null, ) } fun onSkontoActiveChanged(newValue: Boolean) = viewModelScope.launch { - val currentState = stateFlow.value as? SkontoFragmentContract.State.Ready ?: return@launch + val currentState = stateFlow.value as? SkontoScreenState.Ready ?: return@launch val totalAmount = if (newValue) currentState.skontoAmount else currentState.fullAmount val discount = getSkontoDiscountPercentageUseCase.execute( currentState.skontoAmount.value, @@ -146,11 +152,19 @@ internal class SkontoFragmentViewModel( } fun onSkontoAmountFieldChanged(newValue: BigDecimal) = viewModelScope.launch { - val currentState = stateFlow.value as? SkontoFragmentContract.State.Ready ?: return@launch + val currentState = stateFlow.value as? SkontoScreenState.Ready ?: return@launch - if (newValue > currentState.fullAmount.value) { + val skontoAmountValidationError = getSkontoAmountValidationErrorUseCase.execute( + newValue, + currentState.fullAmount.value + ) + + if (skontoAmountValidationError != null) { stateFlow.emit( - currentState.copy(skontoAmount = currentState.skontoAmount) + currentState.copy( + skontoAmount = currentState.skontoAmount, + skontoAmountValidationError = SkontoScreenState.Ready.SkontoAmountValidationError.SkontoAmountMoreThanFullAmount + ) ) return@launch } @@ -176,6 +190,7 @@ internal class SkontoFragmentViewModel( stateFlow.emit( currentState.copy( + skontoAmountValidationError = skontoAmountValidationError, skontoAmount = newSkontoAmount, skontoPercentage = discount, totalAmount = newTotalAmount, @@ -185,7 +200,7 @@ internal class SkontoFragmentViewModel( } fun onSkontoDueDateChanged(newDate: LocalDate) = viewModelScope.launch { - val currentState = stateFlow.value as? SkontoFragmentContract.State.Ready ?: return@launch + val currentState = stateFlow.value as? SkontoScreenState.Ready ?: return@launch val newPayInDays = getSkontoRemainingDaysUseCase.execute(newDate) stateFlow.emit( currentState.copy( @@ -200,7 +215,19 @@ internal class SkontoFragmentViewModel( } fun onFullAmountFieldChanged(newValue: BigDecimal) = viewModelScope.launch { - val currentState = stateFlow.value as? SkontoFragmentContract.State.Ready ?: return@launch + val currentState = stateFlow.value as? SkontoScreenState.Ready ?: return@launch + + val validationError = getFullAmountValidationErrorUseCase.execute(newValue) + + if (validationError != null) { + stateFlow.emit( + currentState.copy( + fullAmountValidationError = validationError + ) + ) + return@launch + } + val totalAmount = if (currentState.isSkontoSectionActive) currentState.skontoAmount.value else newValue @@ -217,6 +244,7 @@ internal class SkontoFragmentViewModel( stateFlow.emit( currentState.copy( + fullAmountValidationError = validationError, skontoAmount = currentState.skontoAmount.copy(value = skontoAmount), fullAmount = currentState.fullAmount.copy(value = newValue), totalAmount = currentState.totalAmount.copy(value = totalAmount), @@ -226,7 +254,7 @@ internal class SkontoFragmentViewModel( } fun onInfoBannerClicked() = viewModelScope.launch { - val currentState = stateFlow.value as? SkontoFragmentContract.State.Ready ?: return@launch + val currentState = stateFlow.value as? SkontoScreenState.Ready ?: return@launch stateFlow.emit( currentState.copy( edgeCaseInfoDialogVisible = true, @@ -235,7 +263,7 @@ internal class SkontoFragmentViewModel( } fun onInfoDialogDismissed() = viewModelScope.launch { - val currentState = stateFlow.value as? SkontoFragmentContract.State.Ready ?: return@launch + val currentState = stateFlow.value as? SkontoScreenState.Ready ?: return@launch stateFlow.emit( currentState.copy( edgeCaseInfoDialogVisible = false, @@ -245,7 +273,7 @@ internal class SkontoFragmentViewModel( fun onInvoiceClicked() = viewModelScope.launch { val currentState = - stateFlow.value as? SkontoFragmentContract.State.Ready ?: return@launch + stateFlow.value as? SkontoScreenState.Ready ?: return@launch val skontoData = SkontoData( skontoAmountToPay = currentState.skontoAmount, skontoDueDate = currentState.discountDueDate, @@ -256,7 +284,7 @@ internal class SkontoFragmentViewModel( ) val documentId = lastAnalyzedDocumentProvider.provide()?.giniApiDocumentId ?: return@launch sideEffectFlow.emit( - SkontoFragmentContract.SideEffect.OpenInvoiceScreen( + SkontoScreenSideEffect.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..2258bbbe9 --- /dev/null +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoScreenContent.kt @@ -0,0 +1,1050 @@ +@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.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.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +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.graphics.vector.rememberVectorPainter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +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.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +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.util.currencyFormatterWithoutSymbol +import net.gini.android.bank.sdk.di.getGiniBankKoin +import net.gini.android.bank.sdk.transactiondocs.ui.dialog.attachdoc.AttachDocumentToTransactionDialog +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.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.compose.koinInject +import java.math.BigDecimal +import java.time.LocalDate +import java.time.format.DateTimeFormatter + +@Composable +internal fun SkontoScreenContent( + amountFormatter: AmountFormatter, + 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 SkontoScreenSideEffect.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, + 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, + customBottomNavBarAdapter: InjectedViewAdapterInstance?, + onInfoBannerClicked: () -> Unit, + onInfoDialogDismissed: () -> Unit, + onInvoiceClicked: () -> Unit, + onConfirmAttachTransactionDocClicked: (alwaysAttach: Boolean) -> Unit, + onCancelAttachTransactionDocClicked: () -> Unit, + 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( + amountFormatter: AmountFormatter, + onConfirmAttachTransactionDocClicked: (alwaysAttach: Boolean) -> Unit, + onCancelAttachTransactionDocClicked: () -> Unit, + onBackClicked: () -> Unit, + onHelpClicked: () -> Unit, + onProceedClicked: () -> Unit, + onInvoiceClicked: () -> Unit, + state: SkontoScreenState.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( + 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.skontoEdgeCase, + 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.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 NavigationActionHelp( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + IconButton( + modifier = modifier + .width(24.dp) + .height(24.dp), + onClick = onClick + ) { + Icon( + painter = painterResource(R.drawable.gbs_help_question_icon), + contentDescription = null, + ) + } +} + +@Composable +private fun NavigationActionBack( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + IconButton( + modifier = modifier + .width(24.dp) + .height(24.dp), + onClick = onClick + ) { + Icon( + painter = rememberVectorPainter(image = Icons.AutoMirrored.Default.ArrowBack), + contentDescription = null, + ) + } +} + +@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 = rememberVectorPainter(image = Icons.AutoMirrored.Default.KeyboardArrowRight), + contentDescription = null, + tint = colorScheme.arrowTint + ) + } + + } +} + +@Composable +private fun SkontoSection( + 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?, + modifier: Modifier = Modifier, + isActive: Boolean, + skontoAmountValidationError: SkontoScreenState.Ready.SkontoAmountValidationError?, + 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( + colors: WithoutSkontoSectionColors, + amount: Amount, + amountFormatter: AmountFormatter, + fullAmountValidationError: SkontoScreenState.Ready.FullAmountValidationError?, + modifier: Modifier = Modifier, + onFullAmountChange: (BigDecimal) -> Unit, + isActive: Boolean, +) { + 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, + 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 = 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, + skontoEdgeCase = SkontoEdgeCase.PayByCashOnly, + edgeCaseInfoDialogVisible = false, + savedAmount = Amount.parse("3:EUR"), + transactionDialogVisible = true, + skontoAmountValidationError = null, + fullAmountValidationError = null, +) + +@Composable +@SuppressLint("ComposableNaming") +private fun SkontoFragmentViewModel.collectSideEffect( + action: (SkontoScreenSideEffect) -> Unit +) { + + val lifecycleOwner = LocalLifecycleOwner.current + + LaunchedEffect(sideEffectFlow, lifecycleOwner) { + lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + sideEffectFlow.collect { + action(it) + } + } + } +} 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..c4a3e9bfd 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 @@ -24,6 +24,8 @@ val skontoScreenModule = module { transactionDocDialogCancelAttachUseCase = get(), getTransactionDocShouldBeAutoAttachedUseCase = get(), getTransactionDocsFeatureEnabledUseCase = get(), + getFullAmountValidationErrorUseCase = get(), + getSkontoAmountValidationErrorUseCase = get(), ) } factory { 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..61892b6e9 --- /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 +} \ No newline at end of file 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..42c1a0f75 --- /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 skontoEdgeCase: SkontoEdgeCase?, + val edgeCaseInfoDialogVisible: Boolean, + val transactionDialogVisible: Boolean, + ) : SkontoScreenState { + + sealed interface SkontoAmountValidationError { + object SkontoAmountMoreThanFullAmount : SkontoAmountValidationError + object SkontoAmountLimitExceeded : SkontoAmountValidationError + } + + sealed interface FullAmountValidationError { + object FullAmountLimitExceeded : FullAmountValidationError + } + } +} \ No newline at end of file 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..5c1b1beca --- /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.usecase.GetSkontoAmountValidationErrorUseCase +import net.gini.android.capture.Amount + +private val maxAmount = + Amount.parse("${GetSkontoAmountValidationErrorUseCase.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) + ) +} \ No newline at end of file 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..522b694ba --- /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.usecase.GetSkontoAmountValidationErrorUseCase +import net.gini.android.capture.Amount + +private val maxAmount = + Amount.parse("${GetSkontoAmountValidationErrorUseCase.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) + ) +} \ No newline at end of file diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetFullAmountValidationErrorUseCase.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetFullAmountValidationErrorUseCase.kt new file mode 100644 index 000000000..029340d70 --- /dev/null +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetFullAmountValidationErrorUseCase.kt @@ -0,0 +1,21 @@ +package net.gini.android.bank.sdk.capture.skonto.usecase + +import net.gini.android.bank.sdk.capture.skonto.SkontoScreenState +import java.math.BigDecimal + +internal class GetFullAmountValidationErrorUseCase { + + fun execute( + fullAmount: BigDecimal + ): SkontoScreenState.Ready.FullAmountValidationError? = when { + + fullAmount > BigDecimal.valueOf(SKONTO_AMOUNT_LIMIT) -> + SkontoScreenState.Ready.FullAmountValidationError.FullAmountLimitExceeded + + else -> null + } + + companion object { + private const val SKONTO_AMOUNT_LIMIT = 99_999L + } +} \ No newline at end of file diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetSkontoAmountValidationErrorUseCase.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetSkontoAmountValidationErrorUseCase.kt new file mode 100644 index 000000000..302b04428 --- /dev/null +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetSkontoAmountValidationErrorUseCase.kt @@ -0,0 +1,24 @@ +package net.gini.android.bank.sdk.capture.skonto.usecase + +import net.gini.android.bank.sdk.capture.skonto.SkontoScreenState +import java.math.BigDecimal + +internal class GetSkontoAmountValidationErrorUseCase { + + fun execute( + newSkontoAmount: BigDecimal, + fullAmount: BigDecimal + ): SkontoScreenState.Ready.SkontoAmountValidationError? = when { + newSkontoAmount > fullAmount -> + SkontoScreenState.Ready.SkontoAmountValidationError.SkontoAmountMoreThanFullAmount + + newSkontoAmount > BigDecimal.valueOf(SKONTO_AMOUNT_LIMIT) -> + SkontoScreenState.Ready.SkontoAmountValidationError.SkontoAmountLimitExceeded + + else -> null + } + + companion object { + internal const val SKONTO_AMOUNT_LIMIT = 99_999L + } +} \ No newline at end of file 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 a5c2efbf8..94aeb4968 100644 --- a/bank-sdk/sdk/src/main/res/values-en/strings.xml +++ b/bank-sdk/sdk/src/main/res/values-en/strings.xml @@ -56,11 +56,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 ac191952a..c561d7459 100644 --- a/bank-sdk/sdk/src/main/res/values/strings.xml +++ b/bank-sdk/sdk/src/main/res/values/strings.xml @@ -56,11 +56,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/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..0359070b8 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 @@ -21,7 +21,7 @@ class DecimalFormatter( fun textToDigits(text: String): String = text.trim() .filter { it != '.' && it != ',' } - .take(7) + .take(8) .trimStart('0') fun parseDigits(digits: String): BigDecimal = 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..2c006bbcd 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( @@ -58,6 +60,15 @@ fun GiniAmountTextInput( currencyCode = currencyCode, isCurrencyCodeDisplay = !enabled, ), + supportingText = supportingText?.let { + { + Text( + text = supportingText, + color = colors.textError, + style = GiniTheme.typography.caption1, + ) + } + } ) } @@ -66,6 +77,7 @@ fun GiniAmountTextInput( @Composable private fun GiniTextInputPreviewLight() { GiniTextInputPreview() + GiniTextInputPreviewError() } @Preview( @@ -75,6 +87,7 @@ private fun GiniTextInputPreviewLight() { @Composable private fun GiniTextInputPreviewDark() { GiniTextInputPreview() + GiniTextInputPreviewError() } @Composable @@ -90,3 +103,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" + ) + } +} + From 252ae9f6ba4cc435390ff5200e6467d7da50145b Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 5 Nov 2024 18:33:37 +0100 Subject: [PATCH 02/24] feature(bank-sdk): Code refactor PP-763 --- .../android/bank/sdk/capture/skonto/SkontoFragment.kt | 1 - .../bank/sdk/capture/skonto/SkontoFragmentViewModel.kt | 3 ++- .../bank/sdk/capture/skonto/SkontoScreenContent.kt | 4 ---- .../bank/sdk/capture/skonto/SkontoScreenSideEffect.kt | 2 +- .../bank/sdk/capture/skonto/SkontoScreenState.kt | 2 +- .../skonto/mapper/FullAmountValidationErrorMapper.kt | 2 +- .../skonto/mapper/SkontoAmountValidationErrorMapper.kt | 2 +- .../usecase/GetFullAmountValidationErrorUseCase.kt | 2 +- .../usecase/GetSkontoAmountValidationErrorUseCase.kt | 2 +- .../ui/components/textinput/amount/DecimalFormatter.kt | 10 ++++++++-- 10 files changed, 16 insertions(+), 14 deletions(-) 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 64269801a..74b069e91 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 @@ -5,7 +5,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.WindowManager -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.fragment.app.Fragment 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 index d1100a1e0..f21f918d5 100644 --- 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 @@ -163,7 +163,8 @@ internal class SkontoFragmentViewModel( stateFlow.emit( currentState.copy( skontoAmount = currentState.skontoAmount, - skontoAmountValidationError = SkontoScreenState.Ready.SkontoAmountValidationError.SkontoAmountMoreThanFullAmount + skontoAmountValidationError = SkontoScreenState.Ready + .SkontoAmountValidationError.SkontoAmountMoreThanFullAmount ) ) return@launch 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 index 2258bbbe9..3c96d7129 100644 --- 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 @@ -78,7 +78,6 @@ 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.util.currencyFormatterWithoutSymbol -import net.gini.android.bank.sdk.di.getGiniBankKoin import net.gini.android.bank.sdk.transactiondocs.ui.dialog.attachdoc.AttachDocumentToTransactionDialog import net.gini.android.capture.Amount import net.gini.android.capture.ui.components.button.filled.GiniButton @@ -92,7 +91,6 @@ 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.compose.koinInject import java.math.BigDecimal import java.time.LocalDate import java.time.format.DateTimeFormatter @@ -985,8 +983,6 @@ private fun ScreenReadyStatePreviewDark() { @Composable private fun ScreenReadyStatePreview() { GiniTheme { - val context = LocalContext.current - var state by remember { mutableStateOf(previewState()) } ScreenReadyState( state = state, 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 index 61892b6e9..9d4556bfb 100644 --- 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 @@ -5,4 +5,4 @@ internal sealed interface SkontoScreenSideEffect { val documentId: String, val infoTextLines: List, ) : SkontoScreenSideEffect -} \ No newline at end of file +} 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 index 42c1a0f75..26e1a23fb 100644 --- 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 @@ -34,4 +34,4 @@ internal sealed interface SkontoScreenState { object FullAmountLimitExceeded : FullAmountValidationError } } -} \ No newline at end of file +} 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 index 5c1b1beca..b1954351e 100644 --- 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 @@ -19,4 +19,4 @@ internal fun SkontoScreenState.Ready.FullAmountValidationError.toErrorMessage( R.string.gbs_skonto_section_without_discount_field_amount_validation_error_limit_exceeded, amountFormatter.format(maxAmount) ) -} \ No newline at end of file +} 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 index 522b694ba..30bd4876d 100644 --- 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 @@ -24,4 +24,4 @@ internal fun SkontoScreenState.Ready.SkontoAmountValidationError.toErrorMessage( R.string.gbs_skonto_section_discount_field_amount_validation_error_limit_exceeded, amountFormatter.format(maxAmount) ) -} \ No newline at end of file +} diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetFullAmountValidationErrorUseCase.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetFullAmountValidationErrorUseCase.kt index 029340d70..dd59d7118 100644 --- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetFullAmountValidationErrorUseCase.kt +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetFullAmountValidationErrorUseCase.kt @@ -18,4 +18,4 @@ internal class GetFullAmountValidationErrorUseCase { companion object { private const val SKONTO_AMOUNT_LIMIT = 99_999L } -} \ No newline at end of file +} diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetSkontoAmountValidationErrorUseCase.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetSkontoAmountValidationErrorUseCase.kt index 302b04428..2bc2bf746 100644 --- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetSkontoAmountValidationErrorUseCase.kt +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetSkontoAmountValidationErrorUseCase.kt @@ -21,4 +21,4 @@ internal class GetSkontoAmountValidationErrorUseCase { companion object { internal const val SKONTO_AMOUNT_LIMIT = 99_999L } -} \ No newline at end of file +} 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 0359070b8..9b8a10e37 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 @@ -16,12 +16,12 @@ class DecimalFormatter( fun parseAmount(amount: BigDecimal) = numberFormat.format(amount).trim() .filter { it != '.' && it != ',' } - .take(7) + .take(MAX_PARSE_LENGTH) .trimStart('0') fun textToDigits(text: String): String = text.trim() .filter { it != '.' && it != ',' } - .take(8) + .take(MAX_FORMAT_LENGTH) .trimStart('0') fun parseDigits(digits: String): BigDecimal = @@ -34,4 +34,10 @@ class DecimalFormatter( // Format to a currency string return numberFormat.format(decimal).trim() } + + companion object { + private const val MAX_PARSE_LENGTH = 7 + private const val MAX_FORMAT_LENGTH = 8 + + } } \ No newline at end of file From 7a841544852d2f0b3392f900191d2871bf025e4f Mon Sep 17 00:00:00 2001 From: Niko Date: Wed, 6 Nov 2024 22:32:17 +0100 Subject: [PATCH 03/24] feature(bank-sdk): Implemented keyboard hide detection PP-763 --- .../capture/skonto/SkontoFragmentViewModel.kt | 11 ++++++ .../sdk/capture/skonto/SkontoScreenContent.kt | 6 ++++ .../android/bank/sdk/util/ui/KeyboardExt.kt | 34 +++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/util/ui/KeyboardExt.kt 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 index f21f918d5..858dc7a82 100644 --- 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 @@ -151,6 +151,17 @@ internal class SkontoFragmentViewModel( ) } + fun onKeyboardStateChanged(isVisible: Boolean) = viewModelScope.launch { + if (isVisible) return@launch + val currentState = stateFlow.value as? SkontoScreenState.Ready ?: return@launch + stateFlow.emit( + currentState.copy( + fullAmountValidationError = null, + skontoAmountValidationError = null + ) + ) + } + fun onSkontoAmountFieldChanged(newValue: BigDecimal) = viewModelScope.launch { val currentState = stateFlow.value as? SkontoScreenState.Ready ?: return@launch 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 index 3c96d7129..15342320f 100644 --- 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 @@ -79,6 +79,7 @@ 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.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 @@ -111,6 +112,11 @@ internal fun SkontoScreenContent( BackHandler { navigateBack() } val state by viewModel.stateFlow.collectAsState() + val keyboardState by keyboardAsState() + + LaunchedEffect(keyboardState) { + viewModel.onKeyboardStateChanged(keyboardState) + } viewModel.collectSideEffect { when (it) { 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) +} From 92ce8c1f63b5fd795eac1090e5f4c323f64499cd Mon Sep 17 00:00:00 2001 From: Niko Date: Wed, 6 Nov 2024 22:56:32 +0100 Subject: [PATCH 04/24] feature(bank-sdk): Added tests PP-763 --- .../GetFullAmountValidationErrorUseCase.kt | 7 +++ .../skonto/SkontoFragmentViewModelTest.kt | 62 +++++++++++++------ ...GetFullAmountValidationErrorUseCaseTest.kt | 24 +++++++ ...tSkontoAmountValidationErrorUseCaseTest.kt | 36 +++++++++++ 4 files changed, 110 insertions(+), 19 deletions(-) create mode 100644 bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetFullAmountValidationErrorUseCaseTest.kt create mode 100644 bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetSkontoAmountValidationErrorUseCaseTest.kt diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetFullAmountValidationErrorUseCase.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetFullAmountValidationErrorUseCase.kt index dd59d7118..c1910c56f 100644 --- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetFullAmountValidationErrorUseCase.kt +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetFullAmountValidationErrorUseCase.kt @@ -3,8 +3,15 @@ package net.gini.android.bank.sdk.capture.skonto.usecase import net.gini.android.bank.sdk.capture.skonto.SkontoScreenState import java.math.BigDecimal +/** + * Use case for validating the full amount to ensure it doesn't exceed the limit. + */ internal class GetFullAmountValidationErrorUseCase { + /** + * Validates the full amount to ensure it doesn't exceed the limit. + */ + fun execute( fullAmount: BigDecimal ): SkontoScreenState.Ready.FullAmountValidationError? = when { 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..074c66951 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 @@ -56,10 +56,12 @@ class SkontoFragmentViewModelTest { transactionDocDialogConfirmAttachUseCase = mockk(), transactionDocDialogCancelAttachUseCase = mockk(), getTransactionDocShouldBeAutoAttachedUseCase = mockk(), + getSkontoAmountValidationErrorUseCase = mockk(), + getFullAmountValidationErrorUseCase = mockk(), ) val flowData = viewModel.stateFlow.first() - assert(flowData is SkontoFragmentContract.State.Ready) + assert(flowData is SkontoScreenState.Ready) } @Test @@ -90,10 +92,12 @@ class SkontoFragmentViewModelTest { transactionDocDialogConfirmAttachUseCase = mockk(), transactionDocDialogCancelAttachUseCase = mockk(), getTransactionDocShouldBeAutoAttachedUseCase = mockk(), + getSkontoAmountValidationErrorUseCase = mockk(), + getFullAmountValidationErrorUseCase = mockk(), ) val flowData = viewModel.stateFlow.first() - assert(flowData is SkontoFragmentContract.State.Ready) + assert(flowData is SkontoScreenState.Ready) coVerify(exactly = 1) { getSkontoSavedAmountUseCase.execute(any(), any()) @@ -124,18 +128,20 @@ class SkontoFragmentViewModelTest { transactionDocDialogConfirmAttachUseCase = mockk(), transactionDocDialogCancelAttachUseCase = mockk(), getTransactionDocShouldBeAutoAttachedUseCase = mockk(), + getSkontoAmountValidationErrorUseCase = mockk(), + getFullAmountValidationErrorUseCase = mockk(), ) with(viewModel.stateFlow.first()) { - assert(this is SkontoFragmentContract.State.Ready) - require(this is SkontoFragmentContract.State.Ready) + assert(this is SkontoScreenState.Ready) + require(this is SkontoScreenState.Ready) } viewModel.onInfoBannerClicked() with(viewModel.stateFlow.first()) { - assert(this is SkontoFragmentContract.State.Ready) - require(this is SkontoFragmentContract.State.Ready) + assert(this is SkontoScreenState.Ready) + require(this is SkontoScreenState.Ready) assert(this.edgeCaseInfoDialogVisible) } } @@ -161,16 +167,18 @@ class SkontoFragmentViewModelTest { transactionDocDialogConfirmAttachUseCase = mockk(), transactionDocDialogCancelAttachUseCase = mockk(), getTransactionDocShouldBeAutoAttachedUseCase = mockk(), + getSkontoAmountValidationErrorUseCase = mockk(), + getFullAmountValidationErrorUseCase = mockk(), ) - viewModel.stateFlow.value = mockk(relaxed = true) + viewModel.stateFlow.value = mockk(relaxed = true) .copy(edgeCaseInfoDialogVisible = true) viewModel.onInfoDialogDismissed() with(viewModel.stateFlow.first()) { - assert(this is SkontoFragmentContract.State.Ready) - require(this is SkontoFragmentContract.State.Ready) + assert(this is SkontoScreenState.Ready) + require(this is SkontoScreenState.Ready) assert(!this.edgeCaseInfoDialogVisible) } } @@ -201,11 +209,13 @@ class SkontoFragmentViewModelTest { transactionDocDialogConfirmAttachUseCase = mockk(), transactionDocDialogCancelAttachUseCase = mockk(), getTransactionDocShouldBeAutoAttachedUseCase = mockk(), + getSkontoAmountValidationErrorUseCase = mockk(), + getFullAmountValidationErrorUseCase = mockk(), ) viewModel.sideEffectFlow.test { viewModel.onInvoiceClicked() - assert(awaitItem() is SkontoFragmentContract.SideEffect.OpenInvoiceScreen) + assert(awaitItem() is SkontoScreenSideEffect.OpenInvoiceScreen) } } @@ -230,14 +240,16 @@ class SkontoFragmentViewModelTest { transactionDocDialogConfirmAttachUseCase = mockk(), transactionDocDialogCancelAttachUseCase = mockk(), getTransactionDocShouldBeAutoAttachedUseCase = mockk(), + getSkontoAmountValidationErrorUseCase = mockk(), + getFullAmountValidationErrorUseCase = 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 is SkontoScreenState.Ready) + require(this is SkontoScreenState.Ready) assert(!this.isSkontoSectionActive) assert(this.totalAmount == this.fullAmount) } @@ -265,14 +277,16 @@ class SkontoFragmentViewModelTest { transactionDocDialogConfirmAttachUseCase = mockk(), transactionDocDialogCancelAttachUseCase = mockk(), getTransactionDocShouldBeAutoAttachedUseCase = mockk(), + getSkontoAmountValidationErrorUseCase = mockk(), + getFullAmountValidationErrorUseCase = mockk(), ) viewModel.stateFlow.test { skipItems(1) // skip initial state viewModel.onSkontoActiveChanged(true) with(awaitItem()) { - assert(this is SkontoFragmentContract.State.Ready) - require(this is SkontoFragmentContract.State.Ready) + assert(this is SkontoScreenState.Ready) + require(this is SkontoScreenState.Ready) assert(this.isSkontoSectionActive) assert(this.totalAmount == this.skontoAmount) } @@ -308,6 +322,8 @@ class SkontoFragmentViewModelTest { transactionDocDialogConfirmAttachUseCase = mockk(), transactionDocDialogCancelAttachUseCase = mockk(), getTransactionDocShouldBeAutoAttachedUseCase = mockk(), + getSkontoAmountValidationErrorUseCase = mockk(), + getFullAmountValidationErrorUseCase = mockk(), ) viewModel.onSkontoAmountFieldChanged(BigDecimal("95")) @@ -346,6 +362,8 @@ class SkontoFragmentViewModelTest { transactionDocDialogConfirmAttachUseCase = mockk(), transactionDocDialogCancelAttachUseCase = mockk(), getTransactionDocShouldBeAutoAttachedUseCase = mockk(), + getSkontoAmountValidationErrorUseCase = mockk(), + getFullAmountValidationErrorUseCase = mockk(), ) viewModel.onSkontoAmountFieldChanged(BigDecimal("110")) @@ -387,6 +405,8 @@ class SkontoFragmentViewModelTest { transactionDocDialogConfirmAttachUseCase = mockk(), transactionDocDialogCancelAttachUseCase = mockk(), getTransactionDocShouldBeAutoAttachedUseCase = mockk(), + getSkontoAmountValidationErrorUseCase = mockk(), + getFullAmountValidationErrorUseCase = mockk(), ) viewModel.onFullAmountFieldChanged(BigDecimal("200")) @@ -405,7 +425,7 @@ class SkontoFragmentViewModelTest { every { execute(any(), any()) } returns mockk() } - val listener = mockk(relaxed = true){ + val listener = mockk(relaxed = true) { every { onPayInvoiceWithSkonto(any(), any()) } just Runs } @@ -427,6 +447,8 @@ class SkontoFragmentViewModelTest { transactionDocDialogConfirmAttachUseCase = mockk(), transactionDocDialogCancelAttachUseCase = mockk(), getTransactionDocShouldBeAutoAttachedUseCase = mockk(), + getSkontoAmountValidationErrorUseCase = mockk(), + getFullAmountValidationErrorUseCase = mockk(), ) viewModel.setListener(listener) @@ -461,6 +483,8 @@ class SkontoFragmentViewModelTest { transactionDocDialogConfirmAttachUseCase = mockk(), transactionDocDialogCancelAttachUseCase = mockk(), getTransactionDocShouldBeAutoAttachedUseCase = mockk(), + getSkontoAmountValidationErrorUseCase = mockk(), + getFullAmountValidationErrorUseCase = mockk(), ) viewModel.stateFlow.test { @@ -468,15 +492,15 @@ class SkontoFragmentViewModelTest { 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 is SkontoScreenState.Ready) + require(this is SkontoScreenState.Ready) assert(this.discountDueDate == futureDueDate) } 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 is SkontoScreenState.Ready) + require(this is SkontoScreenState.Ready) assert(this.discountDueDate == pastDueDate) } } diff --git a/bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetFullAmountValidationErrorUseCaseTest.kt b/bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetFullAmountValidationErrorUseCaseTest.kt new file mode 100644 index 000000000..d7d65fe5e --- /dev/null +++ b/bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetFullAmountValidationErrorUseCaseTest.kt @@ -0,0 +1,24 @@ +package net.gini.android.bank.sdk.capture.skonto.usecase + +import net.gini.android.bank.sdk.capture.skonto.SkontoScreenState +import org.junit.Test +import java.math.BigDecimal + +class GetFullAmountValidationErrorUseCaseTest { + + @Test + fun `full amount validation error should be null if full amount is less than or equal to MAX_AMOUNT`() { + val useCase = GetFullAmountValidationErrorUseCase() + 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 = GetFullAmountValidationErrorUseCase() + 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/bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetSkontoAmountValidationErrorUseCaseTest.kt b/bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetSkontoAmountValidationErrorUseCaseTest.kt new file mode 100644 index 000000000..4518dd53c --- /dev/null +++ b/bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetSkontoAmountValidationErrorUseCaseTest.kt @@ -0,0 +1,36 @@ +package net.gini.android.bank.sdk.capture.skonto.usecase + +import net.gini.android.bank.sdk.capture.skonto.SkontoScreenState +import org.junit.Test +import java.math.BigDecimal + + +class GetSkontoAmountValidationErrorUseCaseTest { + + @Test + fun `skonto amount validation error should be null if skonto amount is less than or equal to full amount`() { + val useCase = GetSkontoAmountValidationErrorUseCase() + 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 = GetSkontoAmountValidationErrorUseCase() + 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 = GetSkontoAmountValidationErrorUseCase() + 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 From 109538e4c55dd221140b3bb7bea6561a9df50d22 Mon Sep 17 00:00:00 2001 From: Niko Date: Thu, 7 Nov 2024 22:17:22 +0100 Subject: [PATCH 05/24] feature(bank-sdk): Tests refactor PP-763 --- .../GetFullAmountValidationErrorUseCase.kt | 2 ++ .../GetSkontoAmountValidationErrorUseCase.kt | 8 ++++++++ .../skonto/SkontoFragmentViewModelTest.kt | 20 ++++++++++++++----- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetFullAmountValidationErrorUseCase.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetFullAmountValidationErrorUseCase.kt index c1910c56f..1f2f4949e 100644 --- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetFullAmountValidationErrorUseCase.kt +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetFullAmountValidationErrorUseCase.kt @@ -10,6 +10,8 @@ internal class GetFullAmountValidationErrorUseCase { /** * Validates the full amount to ensure it doesn't exceed the limit. + * + * @return [SkontoScreenState.Ready.FullAmountValidationError] if the amount is invalid and null otherwise. */ fun execute( diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetSkontoAmountValidationErrorUseCase.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetSkontoAmountValidationErrorUseCase.kt index 2bc2bf746..17b3a3152 100644 --- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetSkontoAmountValidationErrorUseCase.kt +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetSkontoAmountValidationErrorUseCase.kt @@ -3,8 +3,16 @@ package net.gini.android.bank.sdk.capture.skonto.usecase import net.gini.android.bank.sdk.capture.skonto.SkontoScreenState import java.math.BigDecimal +/** + * Use case for validating the Skonto amount. + */ internal class GetSkontoAmountValidationErrorUseCase { + /** + * Validates the Skonto amount. + * + * @return [SkontoScreenState.Ready.SkontoAmountValidationError] if the validation fails, null otherwise. + */ fun execute( newSkontoAmount: BigDecimal, fullAmount: BigDecimal 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 074c66951..49686bdf2 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 @@ -2,6 +2,7 @@ package net.gini.android.bank.sdk.capture.skonto import app.cash.turbine.test import io.mockk.Runs +import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.just @@ -322,8 +323,12 @@ class SkontoFragmentViewModelTest { transactionDocDialogConfirmAttachUseCase = mockk(), transactionDocDialogCancelAttachUseCase = mockk(), getTransactionDocShouldBeAutoAttachedUseCase = mockk(), - getSkontoAmountValidationErrorUseCase = mockk(), - getFullAmountValidationErrorUseCase = mockk(), + getSkontoAmountValidationErrorUseCase = mockk { + coEvery { execute(any(), any()) } returns null + }, + getFullAmountValidationErrorUseCase = mockk { + coEvery { execute(any()) } returns null + }, ) viewModel.onSkontoAmountFieldChanged(BigDecimal("95")) @@ -346,6 +351,7 @@ class SkontoFragmentViewModelTest { every { execute(any(), any()) } returns mockk() } + val viewModel = SkontoFragmentViewModel( data = skontoData, getTransactionDocsFeatureEnabledUseCase = mockk(), @@ -362,7 +368,7 @@ class SkontoFragmentViewModelTest { transactionDocDialogConfirmAttachUseCase = mockk(), transactionDocDialogCancelAttachUseCase = mockk(), getTransactionDocShouldBeAutoAttachedUseCase = mockk(), - getSkontoAmountValidationErrorUseCase = mockk(), + getSkontoAmountValidationErrorUseCase = mockk(relaxed = true), getFullAmountValidationErrorUseCase = mockk(), ) @@ -405,8 +411,12 @@ class SkontoFragmentViewModelTest { transactionDocDialogConfirmAttachUseCase = mockk(), transactionDocDialogCancelAttachUseCase = mockk(), getTransactionDocShouldBeAutoAttachedUseCase = mockk(), - getSkontoAmountValidationErrorUseCase = mockk(), - getFullAmountValidationErrorUseCase = mockk(), + getSkontoAmountValidationErrorUseCase = mockk { + coEvery { execute(any(), any()) } returns null + }, + getFullAmountValidationErrorUseCase = mockk { + coEvery { execute(any()) } returns null + }, ) viewModel.onFullAmountFieldChanged(BigDecimal("200")) From 115cafe4768a79b7e44a6aa3bec461d8d254466f Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 12 Nov 2024 14:53:57 +0100 Subject: [PATCH 06/24] feature(bank-sdk): Skonto + RA validation implementation PP-795 --- .../skonto/DigitalInvoiceSkontoFragment.kt | 18 +++++++++++- .../DigitalInvoiceSkontoScreenModule.kt | 5 +++- .../skonto/DigitalInvoiceSkontoScreenState.kt | 10 +++++-- .../skonto/DigitalInvoiceSkontoViewModel.kt | 29 +++++++++++++++++-- .../SkontoAmountValidationErrorMapper.kt | 14 +++++++++ .../DigitalInvoiceSkontoAmountValidator.kt | 15 ++++++++++ .../textinput/amount/DecimalFormatter.kt | 12 ++------ 7 files changed, 87 insertions(+), 16 deletions(-) create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/mapper/SkontoAmountValidationErrorMapper.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/validation/DigitalInvoiceSkontoAmountValidator.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 d805fbb70..2e6894880 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 @@ -82,10 +82,13 @@ import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.colors.section.Di 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.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.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 @@ -217,6 +220,11 @@ private fun ScreenContent( BackHandler { navigateBack() } val state by viewModel.stateFlow.collectAsState() + val keyboardState by keyboardAsState() + + LaunchedEffect(keyboardState) { + viewModel.onKeyboardStateChanged(keyboardState) + } viewModel.collectSideEffect { when (it) { @@ -240,7 +248,7 @@ private fun ScreenContent( onInfoDialogDismissed = viewModel::onInfoDialogDismissed, onInvoiceClicked = viewModel::onInvoiceClicked, customBottomNavBarAdapter = customBottomNavBarAdapter, - onHelpClicked = viewModel::onHelpClicked + onHelpClicked = viewModel::onHelpClicked, ) } @@ -355,6 +363,7 @@ private fun ScreenReadyState( onDueDateChanged = onDueDateChanged, edgeCase = state.edgeCase, onInfoBannerClicked = onInfoBannerClicked, + skontoAmountValidationError = state.skontoAmountValidationError, ) } } @@ -521,9 +530,11 @@ private fun SkontoSection( onInfoBannerClicked: () -> Unit, edgeCase: SkontoEdgeCase?, colors: DigitalInvoiceSkontoSectionColors, + skontoAmountValidationError: DigitalInvoiceSkontoScreenState.Ready.SkontoAmountValidationError?, modifier: Modifier = Modifier, ) { val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy") + val resources = LocalContext.current.resources var isDatePickerVisible by remember { mutableStateOf(false) } Card( @@ -638,6 +649,10 @@ private fun SkontoSection( ) } }, + isError = skontoAmountValidationError != null, + supportingText = skontoAmountValidationError?.toErrorMessage( + resources = resources, + ) ) val dueDateOnClickSource = remember { MutableInteractionSource() } @@ -888,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..8e039a3a4 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,7 @@ 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 org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module @@ -12,7 +13,9 @@ val digitalInvoiceSkontoScreenModule = module { getSkontoEdgeCaseUseCase = get(), getSkontoRemainingDaysUseCase = get(), lastAnalyzedDocumentProvider = get(), - skontoInvoicePreviewTextLinesFactory = get() + skontoInvoicePreviewTextLinesFactory = get(), + digitalInvoiceSkontoAmountValidator = get(), ) } + factory { DigitalInvoiceSkontoAmountValidator() } } 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/DigitalInvoiceSkontoScreenState.kt index 191168660..a52fdad83 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/DigitalInvoiceSkontoScreenState.kt @@ -6,19 +6,25 @@ import net.gini.android.capture.Amount import java.math.BigDecimal import java.time.LocalDate -internal sealed class DigitalInvoiceSkontoScreenState { +internal sealed interface DigitalInvoiceSkontoScreenState { 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() + ) : DigitalInvoiceSkontoScreenState { + + sealed interface SkontoAmountValidationError { + object SkontoAmountMoreThanFullAmount : SkontoAmountValidationError + } + } } internal sealed interface DigitalInvoiceSkontoSideEffect { 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 index cb5d917f9..3cd92003a 100644 --- 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 @@ -7,6 +7,7 @@ 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.digitalinvoice.skonto.validation.DigitalInvoiceSkontoAmountValidator 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 @@ -23,6 +24,7 @@ internal class DigitalInvoiceSkontoViewModel( private val getSkontoEdgeCaseUseCase: GetSkontoEdgeCaseUseCase, private val getSkontoRemainingDaysUseCase: GetSkontoRemainingDaysUseCase, private val skontoInvoicePreviewTextLinesFactory: SkontoInvoicePreviewTextLinesFactory, + private val digitalInvoiceSkontoAmountValidator: DigitalInvoiceSkontoAmountValidator, ) : ViewModel() { val stateFlow: MutableStateFlow = @@ -70,6 +72,7 @@ internal class DigitalInvoiceSkontoViewModel( paymentMethod = paymentMethod, edgeCase = edgeCase, edgeCaseInfoDialogVisible = edgeCase != null, + skontoAmountValidationError = null, ) } @@ -77,9 +80,18 @@ internal class DigitalInvoiceSkontoViewModel( val currentState = stateFlow.value as? DigitalInvoiceSkontoScreenState.Ready ?: return@launch - if (newValue > currentState.fullAmount.value) { + val skontoAmountValidationError = digitalInvoiceSkontoAmountValidator( + newValue, + currentState.fullAmount.value + ) + + if (skontoAmountValidationError != null) { stateFlow.emit( - currentState.copy(skontoAmount = currentState.skontoAmount) + currentState.copy( + skontoAmount = currentState.skontoAmount, + skontoAmountValidationError = DigitalInvoiceSkontoScreenState.Ready + .SkontoAmountValidationError.SkontoAmountMoreThanFullAmount + ) ) return@launch } @@ -95,10 +107,12 @@ internal class DigitalInvoiceSkontoViewModel( currentState.copy( skontoAmount = newSkontoAmount, skontoPercentage = discount, + skontoAmountValidationError = null, ) ) } + fun onSkontoDueDateChanged(newDate: LocalDate) = viewModelScope.launch { val currentState = stateFlow.value as? DigitalInvoiceSkontoScreenState.Ready ?: return@launch @@ -115,6 +129,17 @@ internal class DigitalInvoiceSkontoViewModel( ) } + fun onKeyboardStateChanged(isVisible: Boolean) = viewModelScope.launch { + if (isVisible) return@launch + val currentState = + stateFlow.value as? DigitalInvoiceSkontoScreenState.Ready ?: return@launch + stateFlow.emit( + currentState.copy( + skontoAmountValidationError = null + ) + ) + } + fun onInfoBannerClicked() = viewModelScope.launch { val currentState = stateFlow.value as? DigitalInvoiceSkontoScreenState.Ready ?: return@launch 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..f2fdda6c5 --- /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.DigitalInvoiceSkontoScreenState + +internal fun DigitalInvoiceSkontoScreenState.Ready.SkontoAmountValidationError.toErrorMessage( + resources: Resources, +): String = when (this) { + is DigitalInvoiceSkontoScreenState.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..71cd8cda1 --- /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.DigitalInvoiceSkontoScreenState +import java.math.BigDecimal + +internal class DigitalInvoiceSkontoAmountValidator { + + operator fun invoke(newSkontoAmount: BigDecimal, fullAmount: BigDecimal) + : DigitalInvoiceSkontoScreenState.Ready.SkontoAmountValidationError? = when { + newSkontoAmount > fullAmount -> + DigitalInvoiceSkontoScreenState.Ready.SkontoAmountValidationError.SkontoAmountMoreThanFullAmount + + else -> null + } +} 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 9b8a10e37..8614b41a8 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() + fun parseAmount(amount: BigDecimal) = numberFormat.format(amount).trim() .filter { it != '.' && it != ',' } - .take(MAX_PARSE_LENGTH) .trimStart('0') fun textToDigits(text: String): String = text.trim() .filter { it != '.' && it != ',' } - .take(MAX_FORMAT_LENGTH) .trimStart('0') fun parseDigits(digits: String): BigDecimal = @@ -34,10 +32,4 @@ class DecimalFormatter( // Format to a currency string return numberFormat.format(decimal).trim() } - - companion object { - private const val MAX_PARSE_LENGTH = 7 - private const val MAX_FORMAT_LENGTH = 8 - - } } \ No newline at end of file From bfd193071e753d71b705902c4245921382106551 Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 12 Nov 2024 17:10:03 +0100 Subject: [PATCH 07/24] feature(bank-sdk): Validators refactor PP-763 --- .../capture/di/skonto/SkontoCommonModule.kt | 10 +--- .../capture/skonto/SkontoFragmentViewModel.kt | 12 ++--- .../sdk/capture/skonto/SkontoScreenModule.kt | 12 ++++- .../mapper/FullAmountValidationErrorMapper.kt | 4 +- .../SkontoAmountValidationErrorMapper.kt | 4 +- .../SkontoAmountValidator.kt} | 12 +---- .../SkontoFullAmountValidator.kt} | 13 +---- .../skonto/SkontoFragmentViewModelTest.kt | 48 +++++++++---------- ...seTest.kt => SkontoAmountValidatorTest.kt} | 9 ++-- ...st.kt => SkontoFullAmountValidatorTest.kt} | 7 +-- 10 files changed, 59 insertions(+), 72 deletions(-) rename bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/{usecase/GetSkontoAmountValidationErrorUseCase.kt => validation/SkontoAmountValidator.kt} (66%) rename bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/{usecase/GetFullAmountValidationErrorUseCase.kt => validation/SkontoFullAmountValidator.kt} (54%) rename bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/skonto/usecase/{GetSkontoAmountValidationErrorUseCaseTest.kt => SkontoAmountValidatorTest.kt} (84%) rename bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/skonto/usecase/{GetFullAmountValidationErrorUseCaseTest.kt => SkontoFullAmountValidatorTest.kt} (77%) diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/di/skonto/SkontoCommonModule.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/di/skonto/SkontoCommonModule.kt index 8f8181f0e..90c68d7aa 100644 --- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/di/skonto/SkontoCommonModule.kt +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/di/skonto/SkontoCommonModule.kt @@ -7,8 +7,8 @@ import net.gini.android.bank.sdk.capture.skonto.factory.text.SkontoSavedAmountTe 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.formatter.SkontoRemainingDaysFormatter -import net.gini.android.bank.sdk.capture.skonto.usecase.GetFullAmountValidationErrorUseCase -import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoAmountValidationErrorUseCase +import net.gini.android.bank.sdk.capture.skonto.validation.SkontoFullAmountValidator +import net.gini.android.bank.sdk.capture.skonto.validation.SkontoAmountValidator import net.gini.android.bank.sdk.capture.util.currencyFormatterWithoutSymbol import org.koin.android.ext.koin.androidContext import org.koin.dsl.module @@ -47,10 +47,4 @@ val skontoCommonModule = module { factory { SkontoDataExtractor() } - factory { - GetSkontoAmountValidationErrorUseCase() - } - factory { - GetFullAmountValidationErrorUseCase() - } } 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 index 858dc7a82..b67af2eba 100644 --- 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 @@ -8,9 +8,9 @@ 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.GetFullAmountValidationErrorUseCase +import net.gini.android.bank.sdk.capture.skonto.validation.SkontoFullAmountValidator import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoAmountUseCase -import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoAmountValidationErrorUseCase +import net.gini.android.bank.sdk.capture.skonto.validation.SkontoAmountValidator 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 @@ -42,8 +42,8 @@ internal class SkontoFragmentViewModel( private val transactionDocDialogCancelAttachUseCase: TransactionDocDialogCancelAttachUseCase, private val getTransactionDocShouldBeAutoAttachedUseCase: GetTransactionDocShouldBeAutoAttachedUseCase, private val getTransactionDocsFeatureEnabledUseCase: GetTransactionDocsFeatureEnabledUseCase, - private val getSkontoAmountValidationErrorUseCase: GetSkontoAmountValidationErrorUseCase, - private val getFullAmountValidationErrorUseCase: GetFullAmountValidationErrorUseCase, + private val skontoAmountValidator: SkontoAmountValidator, + private val skontoFullAmountValidator: SkontoFullAmountValidator, ) : ViewModel() { val stateFlow: MutableStateFlow = @@ -165,7 +165,7 @@ internal class SkontoFragmentViewModel( fun onSkontoAmountFieldChanged(newValue: BigDecimal) = viewModelScope.launch { val currentState = stateFlow.value as? SkontoScreenState.Ready ?: return@launch - val skontoAmountValidationError = getSkontoAmountValidationErrorUseCase.execute( + val skontoAmountValidationError = skontoAmountValidator.execute( newValue, currentState.fullAmount.value ) @@ -229,7 +229,7 @@ internal class SkontoFragmentViewModel( fun onFullAmountFieldChanged(newValue: BigDecimal) = viewModelScope.launch { val currentState = stateFlow.value as? SkontoScreenState.Ready ?: return@launch - val validationError = getFullAmountValidationErrorUseCase.execute(newValue) + val validationError = skontoFullAmountValidator.execute(newValue) if (validationError != null) { stateFlow.emit( 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 c4a3e9bfd..cc854fbfa 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,8 @@ 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 org.koin.android.ext.koin.androidContext import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module @@ -24,8 +26,8 @@ val skontoScreenModule = module { transactionDocDialogCancelAttachUseCase = get(), getTransactionDocShouldBeAutoAttachedUseCase = get(), getTransactionDocsFeatureEnabledUseCase = get(), - getFullAmountValidationErrorUseCase = get(), - getSkontoAmountValidationErrorUseCase = get(), + skontoFullAmountValidator = get(), + skontoAmountValidator = get(), ) } factory { @@ -34,4 +36,10 @@ val skontoScreenModule = module { amountFormatter = get() ) } + factory { + SkontoAmountValidator() + } + factory { + SkontoFullAmountValidator() + } } \ No newline at end of file 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 index b1954351e..7914b9901 100644 --- 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 @@ -4,11 +4,11 @@ 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.usecase.GetSkontoAmountValidationErrorUseCase +import net.gini.android.bank.sdk.capture.skonto.validation.SkontoAmountValidator import net.gini.android.capture.Amount private val maxAmount = - Amount.parse("${GetSkontoAmountValidationErrorUseCase.SKONTO_AMOUNT_LIMIT}:EUR") + Amount.parse("${SkontoAmountValidator.SKONTO_AMOUNT_LIMIT}:EUR") internal fun SkontoScreenState.Ready.FullAmountValidationError.toErrorMessage( resources: Resources, 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 index 30bd4876d..3decb5231 100644 --- 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 @@ -4,11 +4,11 @@ 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.usecase.GetSkontoAmountValidationErrorUseCase +import net.gini.android.bank.sdk.capture.skonto.validation.SkontoAmountValidator import net.gini.android.capture.Amount private val maxAmount = - Amount.parse("${GetSkontoAmountValidationErrorUseCase.SKONTO_AMOUNT_LIMIT}:EUR") + Amount.parse("${SkontoAmountValidator.SKONTO_AMOUNT_LIMIT}:EUR") internal fun SkontoScreenState.Ready.SkontoAmountValidationError.toErrorMessage( resources: Resources, diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetSkontoAmountValidationErrorUseCase.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/validation/SkontoAmountValidator.kt similarity index 66% rename from bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetSkontoAmountValidationErrorUseCase.kt rename to bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/validation/SkontoAmountValidator.kt index 17b3a3152..68dc94403 100644 --- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetSkontoAmountValidationErrorUseCase.kt +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/validation/SkontoAmountValidator.kt @@ -1,18 +1,10 @@ -package net.gini.android.bank.sdk.capture.skonto.usecase +package net.gini.android.bank.sdk.capture.skonto.validation import net.gini.android.bank.sdk.capture.skonto.SkontoScreenState import java.math.BigDecimal -/** - * Use case for validating the Skonto amount. - */ -internal class GetSkontoAmountValidationErrorUseCase { +internal class SkontoAmountValidator { - /** - * Validates the Skonto amount. - * - * @return [SkontoScreenState.Ready.SkontoAmountValidationError] if the validation fails, null otherwise. - */ fun execute( newSkontoAmount: BigDecimal, fullAmount: BigDecimal diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetFullAmountValidationErrorUseCase.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/validation/SkontoFullAmountValidator.kt similarity index 54% rename from bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetFullAmountValidationErrorUseCase.kt rename to bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/validation/SkontoFullAmountValidator.kt index 1f2f4949e..0149c8a35 100644 --- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetFullAmountValidationErrorUseCase.kt +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/validation/SkontoFullAmountValidator.kt @@ -1,18 +1,9 @@ -package net.gini.android.bank.sdk.capture.skonto.usecase +package net.gini.android.bank.sdk.capture.skonto.validation import net.gini.android.bank.sdk.capture.skonto.SkontoScreenState import java.math.BigDecimal -/** - * Use case for validating the full amount to ensure it doesn't exceed the limit. - */ -internal class GetFullAmountValidationErrorUseCase { - - /** - * Validates the full amount to ensure it doesn't exceed the limit. - * - * @return [SkontoScreenState.Ready.FullAmountValidationError] if the amount is invalid and null otherwise. - */ +internal class SkontoFullAmountValidator { fun execute( fullAmount: BigDecimal 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 49686bdf2..89d672926 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 @@ -57,8 +57,8 @@ class SkontoFragmentViewModelTest { transactionDocDialogConfirmAttachUseCase = mockk(), transactionDocDialogCancelAttachUseCase = mockk(), getTransactionDocShouldBeAutoAttachedUseCase = mockk(), - getSkontoAmountValidationErrorUseCase = mockk(), - getFullAmountValidationErrorUseCase = mockk(), + skontoAmountValidator = mockk(), + skontoFullAmountValidator = mockk(), ) val flowData = viewModel.stateFlow.first() @@ -93,8 +93,8 @@ class SkontoFragmentViewModelTest { transactionDocDialogConfirmAttachUseCase = mockk(), transactionDocDialogCancelAttachUseCase = mockk(), getTransactionDocShouldBeAutoAttachedUseCase = mockk(), - getSkontoAmountValidationErrorUseCase = mockk(), - getFullAmountValidationErrorUseCase = mockk(), + skontoAmountValidator = mockk(), + skontoFullAmountValidator = mockk(), ) val flowData = viewModel.stateFlow.first() @@ -129,8 +129,8 @@ class SkontoFragmentViewModelTest { transactionDocDialogConfirmAttachUseCase = mockk(), transactionDocDialogCancelAttachUseCase = mockk(), getTransactionDocShouldBeAutoAttachedUseCase = mockk(), - getSkontoAmountValidationErrorUseCase = mockk(), - getFullAmountValidationErrorUseCase = mockk(), + skontoAmountValidator = mockk(), + skontoFullAmountValidator = mockk(), ) with(viewModel.stateFlow.first()) { @@ -168,8 +168,8 @@ class SkontoFragmentViewModelTest { transactionDocDialogConfirmAttachUseCase = mockk(), transactionDocDialogCancelAttachUseCase = mockk(), getTransactionDocShouldBeAutoAttachedUseCase = mockk(), - getSkontoAmountValidationErrorUseCase = mockk(), - getFullAmountValidationErrorUseCase = mockk(), + skontoAmountValidator = mockk(), + skontoFullAmountValidator = mockk(), ) viewModel.stateFlow.value = mockk(relaxed = true) @@ -210,8 +210,8 @@ class SkontoFragmentViewModelTest { transactionDocDialogConfirmAttachUseCase = mockk(), transactionDocDialogCancelAttachUseCase = mockk(), getTransactionDocShouldBeAutoAttachedUseCase = mockk(), - getSkontoAmountValidationErrorUseCase = mockk(), - getFullAmountValidationErrorUseCase = mockk(), + skontoAmountValidator = mockk(), + skontoFullAmountValidator = mockk(), ) viewModel.sideEffectFlow.test { @@ -241,8 +241,8 @@ class SkontoFragmentViewModelTest { transactionDocDialogConfirmAttachUseCase = mockk(), transactionDocDialogCancelAttachUseCase = mockk(), getTransactionDocShouldBeAutoAttachedUseCase = mockk(), - getSkontoAmountValidationErrorUseCase = mockk(), - getFullAmountValidationErrorUseCase = mockk(), + skontoAmountValidator = mockk(), + skontoFullAmountValidator = mockk(), ) viewModel.stateFlow.test { @@ -278,8 +278,8 @@ class SkontoFragmentViewModelTest { transactionDocDialogConfirmAttachUseCase = mockk(), transactionDocDialogCancelAttachUseCase = mockk(), getTransactionDocShouldBeAutoAttachedUseCase = mockk(), - getSkontoAmountValidationErrorUseCase = mockk(), - getFullAmountValidationErrorUseCase = mockk(), + skontoAmountValidator = mockk(), + skontoFullAmountValidator = mockk(), ) viewModel.stateFlow.test { @@ -323,10 +323,10 @@ class SkontoFragmentViewModelTest { transactionDocDialogConfirmAttachUseCase = mockk(), transactionDocDialogCancelAttachUseCase = mockk(), getTransactionDocShouldBeAutoAttachedUseCase = mockk(), - getSkontoAmountValidationErrorUseCase = mockk { + skontoAmountValidator = mockk { coEvery { execute(any(), any()) } returns null }, - getFullAmountValidationErrorUseCase = mockk { + skontoFullAmountValidator = mockk { coEvery { execute(any()) } returns null }, ) @@ -368,8 +368,8 @@ class SkontoFragmentViewModelTest { transactionDocDialogConfirmAttachUseCase = mockk(), transactionDocDialogCancelAttachUseCase = mockk(), getTransactionDocShouldBeAutoAttachedUseCase = mockk(), - getSkontoAmountValidationErrorUseCase = mockk(relaxed = true), - getFullAmountValidationErrorUseCase = mockk(), + skontoAmountValidator = mockk(relaxed = true), + skontoFullAmountValidator = mockk(), ) viewModel.onSkontoAmountFieldChanged(BigDecimal("110")) @@ -411,10 +411,10 @@ class SkontoFragmentViewModelTest { transactionDocDialogConfirmAttachUseCase = mockk(), transactionDocDialogCancelAttachUseCase = mockk(), getTransactionDocShouldBeAutoAttachedUseCase = mockk(), - getSkontoAmountValidationErrorUseCase = mockk { + skontoAmountValidator = mockk { coEvery { execute(any(), any()) } returns null }, - getFullAmountValidationErrorUseCase = mockk { + skontoFullAmountValidator = mockk { coEvery { execute(any()) } returns null }, ) @@ -457,8 +457,8 @@ class SkontoFragmentViewModelTest { transactionDocDialogConfirmAttachUseCase = mockk(), transactionDocDialogCancelAttachUseCase = mockk(), getTransactionDocShouldBeAutoAttachedUseCase = mockk(), - getSkontoAmountValidationErrorUseCase = mockk(), - getFullAmountValidationErrorUseCase = mockk(), + skontoAmountValidator = mockk(), + skontoFullAmountValidator = mockk(), ) viewModel.setListener(listener) @@ -493,8 +493,8 @@ class SkontoFragmentViewModelTest { transactionDocDialogConfirmAttachUseCase = mockk(), transactionDocDialogCancelAttachUseCase = mockk(), getTransactionDocShouldBeAutoAttachedUseCase = mockk(), - getSkontoAmountValidationErrorUseCase = mockk(), - getFullAmountValidationErrorUseCase = mockk(), + skontoAmountValidator = mockk(), + skontoFullAmountValidator = mockk(), ) viewModel.stateFlow.test { diff --git a/bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetSkontoAmountValidationErrorUseCaseTest.kt b/bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/skonto/usecase/SkontoAmountValidatorTest.kt similarity index 84% rename from bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetSkontoAmountValidationErrorUseCaseTest.kt rename to bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/skonto/usecase/SkontoAmountValidatorTest.kt index 4518dd53c..c583ac142 100644 --- a/bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetSkontoAmountValidationErrorUseCaseTest.kt +++ b/bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/skonto/usecase/SkontoAmountValidatorTest.kt @@ -1,15 +1,16 @@ 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 GetSkontoAmountValidationErrorUseCaseTest { +class SkontoAmountValidatorTest { @Test fun `skonto amount validation error should be null if skonto amount is less than or equal to full amount`() { - val useCase = GetSkontoAmountValidationErrorUseCase() + val useCase = SkontoAmountValidator() val skontoAmount = BigDecimal("100.00") val fullAmount = BigDecimal("200.00") val result = useCase.execute(skontoAmount, fullAmount) @@ -18,7 +19,7 @@ class GetSkontoAmountValidationErrorUseCaseTest { @Test fun `skonto amount validation error should be SKONTO_AMOUNT_MORE_THAN_FULL_AMOUNT if skonto amount is greater than full amount`() { - val useCase = GetSkontoAmountValidationErrorUseCase() + val useCase = SkontoAmountValidator() val skontoAmount = BigDecimal("300.00") val fullAmount = BigDecimal("200.00") val result = useCase.execute(skontoAmount, fullAmount) @@ -27,7 +28,7 @@ class GetSkontoAmountValidationErrorUseCaseTest { @Test fun `skonto amount validation error should be SKONTO_AMOUNT_LIMIT_EXCEEDED if skonto amount is greater than MAX_AMOUNT`() { - val useCase = GetSkontoAmountValidationErrorUseCase() + val useCase = SkontoAmountValidator() val skontoAmount = BigDecimal("1000000.00") val fullAmount = BigDecimal("1000000.00") val result = useCase.execute(skontoAmount, fullAmount) diff --git a/bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetFullAmountValidationErrorUseCaseTest.kt b/bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/skonto/usecase/SkontoFullAmountValidatorTest.kt similarity index 77% rename from bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetFullAmountValidationErrorUseCaseTest.kt rename to bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/skonto/usecase/SkontoFullAmountValidatorTest.kt index d7d65fe5e..5ebd9b5d6 100644 --- a/bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/skonto/usecase/GetFullAmountValidationErrorUseCaseTest.kt +++ b/bank-sdk/sdk/src/test/java/net/gini/android/bank/sdk/capture/skonto/usecase/SkontoFullAmountValidatorTest.kt @@ -1,14 +1,15 @@ 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 GetFullAmountValidationErrorUseCaseTest { +class SkontoFullAmountValidatorTest { @Test fun `full amount validation error should be null if full amount is less than or equal to MAX_AMOUNT`() { - val useCase = GetFullAmountValidationErrorUseCase() + val useCase = SkontoFullAmountValidator() val fullAmount = BigDecimal("100.00") val result = useCase.execute(fullAmount) assert(result == null) @@ -16,7 +17,7 @@ class GetFullAmountValidationErrorUseCaseTest { @Test fun `full amount validation error should be MAX_AMOUNT_EXCEEDED if full amount is greater than MAX_AMOUNT`() { - val useCase = GetFullAmountValidationErrorUseCase() + val useCase = SkontoFullAmountValidator() val fullAmount = BigDecimal("1000000.00") val result = useCase.execute(fullAmount) assert(result == SkontoScreenState.Ready.FullAmountValidationError.FullAmountLimitExceeded) From c79be129131d4183dec30afcbcf9867d109e232b Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 12 Nov 2024 17:12:54 +0100 Subject: [PATCH 08/24] feature(bank-sdk): Code refactor PP-795 --- .../android/bank/sdk/capture/di/skonto/SkontoCommonModule.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/di/skonto/SkontoCommonModule.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/di/skonto/SkontoCommonModule.kt index 90c68d7aa..d4dd7b994 100644 --- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/di/skonto/SkontoCommonModule.kt +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/di/skonto/SkontoCommonModule.kt @@ -7,8 +7,6 @@ import net.gini.android.bank.sdk.capture.skonto.factory.text.SkontoSavedAmountTe 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.formatter.SkontoRemainingDaysFormatter -import net.gini.android.bank.sdk.capture.skonto.validation.SkontoFullAmountValidator -import net.gini.android.bank.sdk.capture.skonto.validation.SkontoAmountValidator import net.gini.android.bank.sdk.capture.util.currencyFormatterWithoutSymbol import org.koin.android.ext.koin.androidContext import org.koin.dsl.module From 67e6ec5a82488a7e19802345255298a4c18e0c93 Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 12 Nov 2024 17:13:28 +0100 Subject: [PATCH 09/24] feature(bank-sdk): Code refactor PP-795 --- .../android/bank/sdk/capture/di/skonto/SkontoCommonModule.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/di/skonto/SkontoCommonModule.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/di/skonto/SkontoCommonModule.kt index 90c68d7aa..d4dd7b994 100644 --- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/di/skonto/SkontoCommonModule.kt +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/di/skonto/SkontoCommonModule.kt @@ -7,8 +7,6 @@ import net.gini.android.bank.sdk.capture.skonto.factory.text.SkontoSavedAmountTe 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.formatter.SkontoRemainingDaysFormatter -import net.gini.android.bank.sdk.capture.skonto.validation.SkontoFullAmountValidator -import net.gini.android.bank.sdk.capture.skonto.validation.SkontoAmountValidator import net.gini.android.bank.sdk.capture.util.currencyFormatterWithoutSymbol import org.koin.android.ext.koin.androidContext import org.koin.dsl.module From 6897ffe3a5d61bc5125b3f4d56b3c3026d9703fb Mon Sep 17 00:00:00 2001 From: Niko Date: Wed, 13 Nov 2024 16:49:52 +0100 Subject: [PATCH 10/24] feature(bank-sdk): Skonto. MVI Orbit implementation PP-763 --- bank-sdk/sdk/build.gradle.kts | 4 + bank-sdk/sdk/detekt-baseline.xml | 2 +- .../bank/sdk/capture/skonto/SkontoFragment.kt | 1 + .../capture/skonto/SkontoFragmentViewModel.kt | 305 ------------------ .../sdk/capture/skonto/SkontoScreenContent.kt | 39 +-- .../sdk/capture/skonto/SkontoScreenModule.kt | 102 +++++- .../viewmodel/SkontoFragmentViewModel.kt | 82 +++++ .../SkontoScreenInitialStateFactory.kt | 51 +++ .../intent/FullAmountChangeIntent.kt | 55 ++++ .../intent/InfoBannerInteractionIntent.kt | 25 ++ .../viewmodel/intent/InvoiceClickIntent.kt | 35 ++ .../intent/KeyboardStateChangeIntent.kt | 18 ++ .../viewmodel/intent/ProceedClickedIntent.kt | 36 +++ .../intent/SkontoActiveChangeIntent.kt | 27 ++ .../intent/SkontoAmountFieldChangeIntent.kt | 65 ++++ .../intent/SkontoDueDateChangeIntent.kt | 28 ++ .../TransactionDocDialogDecisionIntent.kt | 27 ++ .../OpenExtractionsScreenSubIntent.kt | 32 ++ .../skonto/SkontoFragmentViewModelTest.kt | 1 + gradle/libs.versions.toml | 6 +- 20 files changed, 590 insertions(+), 351 deletions(-) delete mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragmentViewModel.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/SkontoFragmentViewModel.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/SkontoScreenInitialStateFactory.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/FullAmountChangeIntent.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/InfoBannerInteractionIntent.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/InvoiceClickIntent.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/KeyboardStateChangeIntent.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/ProceedClickedIntent.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/SkontoActiveChangeIntent.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/SkontoAmountFieldChangeIntent.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/SkontoDueDateChangeIntent.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/TransactionDocDialogDecisionIntent.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/subintent/OpenExtractionsScreenSubIntent.kt 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/skonto/SkontoFragment.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragment.kt index 74b069e91..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 @@ -13,6 +13,7 @@ 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.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.util.disallowScreenshots import net.gini.android.capture.GiniCapture 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 b67af2eba..000000000 --- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoFragmentViewModel.kt +++ /dev/null @@ -1,305 +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.validation.SkontoFullAmountValidator -import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoAmountUseCase -import net.gini.android.bank.sdk.capture.skonto.validation.SkontoAmountValidator -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, - private val skontoAmountValidator: SkontoAmountValidator, - private val skontoFullAmountValidator: SkontoFullAmountValidator, -) : 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? SkontoScreenState.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? SkontoScreenState.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, - ): 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, - skontoEdgeCase = edgeCase, - edgeCaseInfoDialogVisible = edgeCase != null, - savedAmount = savedAmount, - transactionDialogVisible = false, - skontoAmountValidationError = null, - fullAmountValidationError = null, - ) - } - - fun onSkontoActiveChanged(newValue: Boolean) = viewModelScope.launch { - val currentState = stateFlow.value as? SkontoScreenState.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 onKeyboardStateChanged(isVisible: Boolean) = viewModelScope.launch { - if (isVisible) return@launch - val currentState = stateFlow.value as? SkontoScreenState.Ready ?: return@launch - stateFlow.emit( - currentState.copy( - fullAmountValidationError = null, - skontoAmountValidationError = null - ) - ) - } - - fun onSkontoAmountFieldChanged(newValue: BigDecimal) = viewModelScope.launch { - val currentState = stateFlow.value as? SkontoScreenState.Ready ?: return@launch - - val skontoAmountValidationError = skontoAmountValidator.execute( - newValue, - currentState.fullAmount.value - ) - - if (skontoAmountValidationError != null) { - stateFlow.emit( - currentState.copy( - skontoAmount = currentState.skontoAmount, - skontoAmountValidationError = SkontoScreenState.Ready - .SkontoAmountValidationError.SkontoAmountMoreThanFullAmount - ) - ) - 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( - skontoAmountValidationError = skontoAmountValidationError, - skontoAmount = newSkontoAmount, - skontoPercentage = discount, - totalAmount = newTotalAmount, - savedAmount = savedAmount, - ) - ) - } - - fun onSkontoDueDateChanged(newDate: LocalDate) = viewModelScope.launch { - val currentState = stateFlow.value as? SkontoScreenState.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? SkontoScreenState.Ready ?: return@launch - - val validationError = skontoFullAmountValidator.execute(newValue) - - if (validationError != null) { - stateFlow.emit( - currentState.copy( - fullAmountValidationError = validationError - ) - ) - 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( - fullAmountValidationError = validationError, - 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? SkontoScreenState.Ready ?: return@launch - stateFlow.emit( - currentState.copy( - edgeCaseInfoDialogVisible = true, - ) - ) - } - - fun onInfoDialogDismissed() = viewModelScope.launch { - val currentState = stateFlow.value as? SkontoScreenState.Ready ?: return@launch - stateFlow.emit( - currentState.copy( - edgeCaseInfoDialogVisible = false, - ) - ) - } - - fun onInvoiceClicked() = viewModelScope.launch { - val currentState = - stateFlow.value as? SkontoScreenState.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( - SkontoScreenSideEffect.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 index 15342320f..cf9722a47 100644 --- 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 @@ -2,7 +2,6 @@ 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.widget.FrameLayout @@ -43,7 +42,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 @@ -54,7 +52,6 @@ import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource @@ -63,8 +60,6 @@ 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.lifecycle.Lifecycle -import androidx.lifecycle.repeatOnLifecycle 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 @@ -77,6 +72,7 @@ import net.gini.android.bank.sdk.capture.skonto.formatter.SkontoDiscountPercenta 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 @@ -92,6 +88,8 @@ 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.orbitmvi.orbit.compose.collectAsState +import org.orbitmvi.orbit.compose.collectSideEffect import java.math.BigDecimal import java.time.LocalDate import java.time.format.DateTimeFormatter @@ -111,13 +109,7 @@ internal fun SkontoScreenContent( BackHandler { navigateBack() } - val state by viewModel.stateFlow.collectAsState() - val keyboardState by keyboardAsState() - - LaunchedEffect(keyboardState) { - viewModel.onKeyboardStateChanged(keyboardState) - } - + val state by viewModel.collectAsState() viewModel.collectSideEffect { when (it) { is SkontoScreenSideEffect.OpenInvoiceScreen -> @@ -125,6 +117,12 @@ internal fun SkontoScreenContent( } } + val keyboardState by keyboardAsState() + + LaunchedEffect(keyboardState) { + viewModel.onKeyboardStateChanged(keyboardState) + } + ScreenStateContent( modifier = modifier, state = state, @@ -1033,20 +1031,3 @@ private fun previewState() = SkontoScreenState.Ready( skontoAmountValidationError = null, fullAmountValidationError = null, ) - -@Composable -@SuppressLint("ComposableNaming") -private fun SkontoFragmentViewModel.collectSideEffect( - action: (SkontoScreenSideEffect) -> Unit -) { - - val lifecycleOwner = LocalLifecycleOwner.current - - LaunchedEffect(sideEffectFlow, lifecycleOwner) { - lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { - sideEffectFlow.collect { - action(it) - } - } - } -} 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 cc854fbfa..c0c14bdb6 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 @@ -4,6 +4,18 @@ import net.gini.android.bank.sdk.capture.skonto.factory.lines.SkontoInvoicePrevi 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 @@ -12,34 +24,94 @@ val skontoScreenModule = module { viewModel { (data: SkontoData) -> SkontoFragmentViewModel( data = data, - getSkontoAmountUseCase = get(), + 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( getSkontoDiscountPercentageUseCase = get(), - getSkontoEdgeCaseUseCase = get(), getSkontoSavedAmountUseCase = get(), - getSkontoRemainingDaysUseCase = get(), - getSkontoDefaultSelectionStateUseCase = get(), - skontoExtractionsHandler = get(), - lastAnalyzedDocumentProvider = get(), - skontoInvoicePreviewTextLinesFactory = get(), - lastExtractionsProvider = get(), - transactionDocDialogConfirmAttachUseCase = get(), - transactionDocDialogCancelAttachUseCase = 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(), + getSkontoSavedAmountUseCase = get(), ) } factory { - SkontoInvoicePreviewTextLinesFactory( - resources = androidContext().resources, - amountFormatter = get() + SkontoDueDateChangeIntent( + getSkontoRemainingDaysUseCase = get(), + getSkontoEdgeCaseUseCase = get(), ) } factory { - SkontoAmountValidator() + TransactionDocDialogDecisionIntent( + openExtractionsScreenSubIntent = get(), + transactionDocDialogConfirmAttachUseCase = get(), + transactionDocDialogCancelAttachUseCase = get(), + ) } factory { - SkontoFullAmountValidator() + 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/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..59c75b450 --- /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, + skontoEdgeCase = 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..81834a548 --- /dev/null +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/FullAmountChangeIntent.kt @@ -0,0 +1,55 @@ +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 + + 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..a494183d5 --- /dev/null +++ b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/viewmodel/intent/SkontoAmountFieldChangeIntent.kt @@ -0,0 +1,65 @@ +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 + + 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..84721c638 --- /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, + skontoEdgeCase = 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/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 89d672926..46e242adc 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 @@ -18,6 +18,7 @@ import net.gini.android.bank.sdk.capture.skonto.usecase.GetSkontoDefaultSelectio 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.GetSkontoSavedAmountUseCase +import net.gini.android.bank.sdk.capture.skonto.viewmodel.SkontoFragmentViewModel import net.gini.android.capture.Amount import net.gini.android.capture.analysis.LastAnalyzedDocumentProvider import org.junit.Rule 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 From 84f627cf5de140272f95b44996c28d94086a1e9e Mon Sep 17 00:00:00 2001 From: Niko Date: Fri, 15 Nov 2024 11:10:23 +0100 Subject: [PATCH 11/24] feature(bank-sdk): Skonto. Code refactor PP-763 --- .../android/bank/sdk/capture/skonto/SkontoFragmentViewModel.kt | 2 ++ 1 file changed, 2 insertions(+) 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 index b67af2eba..2092142e4 100644 --- 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 @@ -165,6 +165,8 @@ internal class SkontoFragmentViewModel( fun onSkontoAmountFieldChanged(newValue: BigDecimal) = viewModelScope.launch { val currentState = stateFlow.value as? SkontoScreenState.Ready ?: return@launch + if (newValue == currentState.skontoAmount.value) return@launch + val skontoAmountValidationError = skontoAmountValidator.execute( newValue, currentState.fullAmount.value From c0418d5bcaf4400dc83923ad282789ddaa3f9168 Mon Sep 17 00:00:00 2001 From: Niko Date: Fri, 15 Nov 2024 11:14:58 +0100 Subject: [PATCH 12/24] feature(bank-sdk): Skonto. Code refactor PP-763 --- .../android/bank/sdk/capture/skonto/SkontoFragmentViewModel.kt | 2 ++ 1 file changed, 2 insertions(+) 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 index 2092142e4..4bd35a50a 100644 --- 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 @@ -231,6 +231,8 @@ internal class SkontoFragmentViewModel( fun onFullAmountFieldChanged(newValue: BigDecimal) = viewModelScope.launch { val currentState = stateFlow.value as? SkontoScreenState.Ready ?: return@launch + if (newValue == currentState.fullAmount.value) return@launch + val validationError = skontoFullAmountValidator.execute(newValue) if (validationError != null) { From fa382b0adcca4e84a138dff1d28fd6a698e6c4db Mon Sep 17 00:00:00 2001 From: Niko Date: Sun, 17 Nov 2024 22:35:10 +0100 Subject: [PATCH 13/24] feature(bank-sdk): Skonto. Code refactor PP-763 --- .../sdk/capture/skonto/SkontoScreenContent.kt | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) 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 index 15342320f..23dc6484c 100644 --- 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 @@ -98,15 +98,15 @@ import java.time.format.DateTimeFormatter @Composable internal fun SkontoScreenContent( - amountFormatter: AmountFormatter, + isBottomNavigationBarEnabled: Boolean, navigateBack: () -> Unit, navigateToHelp: () -> Unit, + amountFormatter: AmountFormatter, viewModel: SkontoFragmentViewModel, - modifier: Modifier = Modifier, - screenColorScheme: SkontoScreenColors = SkontoScreenColors.colors(), - isBottomNavigationBarEnabled: Boolean, customBottomNavBarAdapter: InjectedViewAdapterInstance?, navigateToInvoiceScreen: (documentId: String, infoTextLines: List) -> Unit, + modifier: Modifier = Modifier, + screenColorScheme: SkontoScreenColors = SkontoScreenColors.colors(), ) { BackHandler { navigateBack() } @@ -159,12 +159,12 @@ private fun ScreenStateContent( onHelpClicked: () -> Unit, onProceedClicked: () -> Unit, isBottomNavigationBarEnabled: Boolean, - customBottomNavBarAdapter: InjectedViewAdapterInstance?, onInfoBannerClicked: () -> Unit, onInfoDialogDismissed: () -> Unit, onInvoiceClicked: () -> Unit, onConfirmAttachTransactionDocClicked: (alwaysAttach: Boolean) -> Unit, onCancelAttachTransactionDocClicked: () -> Unit, + customBottomNavBarAdapter: InjectedViewAdapterInstance?, modifier: Modifier = Modifier, screenColorScheme: SkontoScreenColors = SkontoScreenColors.colors() ) { @@ -195,6 +195,8 @@ private fun ScreenStateContent( @Composable private fun ScreenReadyState( + isBottomNavigationBarEnabled: Boolean, + state: SkontoScreenState.Ready, amountFormatter: AmountFormatter, onConfirmAttachTransactionDocClicked: (alwaysAttach: Boolean) -> Unit, onCancelAttachTransactionDocClicked: () -> Unit, @@ -202,16 +204,14 @@ private fun ScreenReadyState( onHelpClicked: () -> Unit, onProceedClicked: () -> Unit, onInvoiceClicked: () -> Unit, - state: SkontoScreenState.Ready, onDiscountSectionActiveChange: (Boolean) -> Unit, onDiscountAmountChange: (BigDecimal) -> Unit, onDueDateChanged: (LocalDate) -> Unit, onFullAmountChange: (BigDecimal) -> Unit, - isBottomNavigationBarEnabled: Boolean, - customBottomNavBarAdapter: InjectedViewAdapterInstance?, - modifier: Modifier = Modifier, onInfoBannerClicked: () -> Unit, onInfoDialogDismissed: () -> Unit, + customBottomNavBarAdapter: InjectedViewAdapterInstance?, + modifier: Modifier = Modifier, discountPercentageFormatter: SkontoDiscountPercentageFormatter = SkontoDiscountPercentageFormatter(), screenColorScheme: SkontoScreenColors = SkontoScreenColors.colors(), ) { @@ -336,9 +336,9 @@ private fun ScreenReadyState( private fun TopAppBar( onBackClicked: () -> Unit, onHelpClicked: () -> Unit, - modifier: Modifier = Modifier, - isBottomNavigationBarEnabled: Boolean, colors: GiniTopBarColors, + isBottomNavigationBarEnabled: Boolean, + modifier: Modifier = Modifier, ) { GiniTopBar( modifier = modifier, @@ -400,9 +400,9 @@ private fun NavigationActionBack( @Composable private fun InvoicePreviewSection( - modifier: Modifier = Modifier, - colorScheme: SkontoInvoicePreviewSectionColors, onClick: () -> Unit, + colorScheme: SkontoInvoicePreviewSectionColors, + modifier: Modifier = Modifier, ) { Card( modifier = modifier @@ -459,6 +459,7 @@ private fun InvoicePreviewSection( @Composable private fun SkontoSection( + isActive: Boolean, amountFormatter: AmountFormatter, colors: SkontoSectionColors, amount: Amount, @@ -470,9 +471,8 @@ private fun SkontoSection( onDueDateChanged: (LocalDate) -> Unit, onInfoBannerClicked: () -> Unit, edgeCase: SkontoEdgeCase?, - modifier: Modifier = Modifier, - isActive: Boolean, skontoAmountValidationError: SkontoScreenState.Ready.SkontoAmountValidationError?, + modifier: Modifier = Modifier, discountPercentageFormatter: SkontoDiscountPercentageFormatter = SkontoDiscountPercentageFormatter(), ) { val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy") @@ -752,13 +752,13 @@ private fun InfoDialog( @Composable private fun WithoutSkontoSection( + isActive: Boolean, + onFullAmountChange: (BigDecimal) -> Unit, colors: WithoutSkontoSectionColors, amount: Amount, amountFormatter: AmountFormatter, fullAmountValidationError: SkontoScreenState.Ready.FullAmountValidationError?, modifier: Modifier = Modifier, - onFullAmountChange: (BigDecimal) -> Unit, - isActive: Boolean, ) { val resources = LocalContext.current.resources @@ -828,9 +828,9 @@ private fun FooterSection( onBackClicked: () -> Unit, onHelpClicked: () -> Unit, onProceedClicked: () -> Unit, - modifier: Modifier = Modifier, - customBottomNavBarAdapter: InjectedViewAdapterInstance?, discountPercentageFormatter: SkontoDiscountPercentageFormatter, + customBottomNavBarAdapter: InjectedViewAdapterInstance?, + modifier: Modifier = Modifier, ) { val animatedTotalAmount by animateFloatAsState( targetValue = totalAmount.value.toFloat(), label = "totalAmount" From 9aa0ad72dfdc313b6b5685cdb69f6163765a3edf Mon Sep 17 00:00:00 2001 From: Niko Date: Mon, 18 Nov 2024 08:36:06 +0100 Subject: [PATCH 14/24] feature(bank-sdk): Skonto. Tests fix PP-763 --- .../sdk/capture/skonto/SkontoScreenModule.kt | 1 - .../skonto/SkontoFragmentViewModelTest.kt | 709 ++++++++++-------- 2 files changed, 413 insertions(+), 297 deletions(-) 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 c0c14bdb6..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 @@ -50,7 +50,6 @@ val skontoScreenModule = module { } factory { SkontoScreenInitialStateFactory( - getSkontoDiscountPercentageUseCase = get(), getSkontoSavedAmountUseCase = get(), getSkontoEdgeCaseUseCase = get(), getSkontoDefaultSelectionStateUseCase = get() 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 46e242adc..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,14 +1,11 @@ package net.gini.android.bank.sdk.capture.skonto -import app.cash.turbine.test import io.mockk.Runs -import io.mockk.coEvery 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 @@ -17,12 +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 @@ -42,28 +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(), - skontoAmountValidator = mockk(), - skontoFullAmountValidator = mockk(), ) - val flowData = viewModel.stateFlow.first() - assert(flowData is SkontoScreenState.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 @@ -78,28 +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(), - skontoAmountValidator = mockk(), - skontoFullAmountValidator = mockk(), ) - val flowData = viewModel.stateFlow.first() - assert(flowData is SkontoScreenState.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()) @@ -114,37 +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(), - skontoAmountValidator = mockk(), - skontoFullAmountValidator = 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 SkontoScreenState.Ready) - require(this is SkontoScreenState.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 SkontoScreenState.Ready) - require(this is SkontoScreenState.Ready) - assert(this.edgeCaseInfoDialogVisible) + viewModel.test(this) { + runOnCreate() + containerHost.onInfoBannerClicked() + expectState { + (this as SkontoScreenState.Ready).copy(edgeCaseInfoDialogVisible = true) + } } } @@ -153,35 +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(), - skontoAmountValidator = mockk(), - skontoFullAmountValidator = 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 SkontoScreenState.Ready) - require(this is SkontoScreenState.Ready) - assert(!this.edgeCaseInfoDialogVisible) + viewModel.test(this) { + runOnCreate() + expectInitialState() + containerHost.onInfoDialogDismissed() + expectState { + (this as SkontoScreenState.Ready).copy(edgeCaseInfoDialogVisible = false) + } } } @@ -189,108 +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(), - skontoAmountValidator = mockk(), - skontoFullAmountValidator = mockk(), ) - viewModel.sideEffectFlow.test { - viewModel.onInvoiceClicked() - assert(awaitItem() is SkontoScreenSideEffect.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(), - skontoAmountValidator = mockk(), - skontoFullAmountValidator = 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 SkontoScreenState.Ready) - require(this is SkontoScreenState.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(), - skontoAmountValidator = mockk(), - skontoFullAmountValidator = 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 SkontoScreenState.Ready) - require(this is SkontoScreenState.Ready) - assert(this.isSkontoSectionActive) - assert(this.totalAmount == this.skontoAmount) + expectState { + (this as SkontoScreenState.Ready).copy( + isSkontoSectionActive = true, + totalAmount = skontoAmount, + skontoPercentage = BigDecimal.ZERO + ) } } } @@ -300,85 +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(), - skontoAmountValidator = mockk { - coEvery { execute(any(), any()) } returns null - }, - skontoFullAmountValidator = mockk { - coEvery { execute(any()) } returns null - }, + 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(), - skontoAmountValidator = mockk(relaxed = true), - skontoFullAmountValidator = mockk(), ) - viewModel.onSkontoAmountFieldChanged(BigDecimal("110")) + val skontoAmountFieldChangeIntent = SkontoAmountFieldChangeIntent( + skontoAmountValidator = SkontoAmountValidator(), + getSkontoDiscountPercentageUseCase = getSkontoDiscountPercentageUseCase, + getSkontoSavedAmountUseCase = mockk(), + ) - coVerify(exactly = 0) { - getSkontoDiscountPercentageUseCase.execute( - any(), any() - ) + 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(), + ) + + viewModel.test(this) { + expectInitialState() + runOnCreate() + containerHost.onSkontoAmountFieldChanged(BigDecimal("110")) + expectState { + with(this as SkontoScreenState.Ready) { + copy( + skontoAmountValidationError = SkontoScreenState + .Ready.SkontoAmountValidationError.SkontoAmountMoreThanFullAmount + ) + } + } } } @@ -386,41 +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(), - skontoAmountValidator = mockk { - coEvery { execute(any(), any()) } returns null - }, - skontoFullAmountValidator = mockk { - coEvery { execute(any()) } returns null - }, + 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()) @@ -430,41 +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) { 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(), - skontoAmountValidator = mockk(), - skontoFullAmountValidator = 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()) @@ -474,45 +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(), - skontoAmountValidator = mockk(), - skontoFullAmountValidator = mockk(), ) - viewModel.stateFlow.test { - skipItems(1) // skip initial state - val futureDueDate = LocalDate.now().plusDays(5) - viewModel.onSkontoDueDateChanged(futureDueDate) - with(awaitItem()) { - assert(this is SkontoScreenState.Ready) - require(this is SkontoScreenState.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 SkontoScreenState.Ready) - require(this is SkontoScreenState.Ready) - assert(this.discountDueDate == pastDueDate) + containerHost.onSkontoDueDateChanged(pastDueDate) + expectState { + (this as SkontoScreenState.Ready).copy( + discountDueDate = pastDueDate, + paymentInDays = 5 // always absolute value + ) } } } From ca620ae624e54648de513e363b57d40694fcf505 Mon Sep 17 00:00:00 2001 From: Niko Date: Mon, 18 Nov 2024 15:02:34 +0100 Subject: [PATCH 15/24] feature(bank-sdk): Skonto + RA. MVI orbit update PP-795 --- .../skonto/DigitalInvoiceSkontoFragment.kt | 61 ++---- .../DigitalInvoiceSkontoScreenModule.kt | 44 +++- .../skonto/DigitalInvoiceSkontoViewModel.kt | 191 ------------------ ...ntoScreenState.kt => SkontoScreenState.kt} | 11 +- .../digitalinvoice/skonto/SkontoSideEffect.kt | 12 ++ .../SkontoAmountValidationErrorMapper.kt | 6 +- .../DigitalInvoiceSkontoAmountValidator.kt | 6 +- .../DigitalInvoiceSkontoViewModel.kt | 60 ++++++ .../SkontoScreenInitialStateFactory.kt | 30 +++ .../viewmodel/intent/BackClickIntent.kt | 27 +++ .../intent/InfoBannerInteractionIntent.kt | 25 +++ .../viewmodel/intent/InvoiceClickIntent.kt | 34 ++++ .../intent/KeyboardStateChangeIntent.kt | 17 ++ .../intent/SkontoAmountFieldChangeIntent.kt | 50 +++++ .../intent/SkontoDueDateChangeIntent.kt | 28 +++ .../sdk/capture/skonto/SkontoScreenContent.kt | 6 +- .../sdk/capture/skonto/SkontoScreenState.kt | 2 +- .../SkontoScreenInitialStateFactory.kt | 2 +- .../intent/SkontoDueDateChangeIntent.kt | 2 +- 19 files changed, 359 insertions(+), 255 deletions(-) delete mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/DigitalInvoiceSkontoViewModel.kt rename bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/{DigitalInvoiceSkontoScreenState.kt => SkontoScreenState.kt} (72%) create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/SkontoSideEffect.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/viewmodel/DigitalInvoiceSkontoViewModel.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/viewmodel/SkontoScreenInitialStateFactory.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/viewmodel/intent/BackClickIntent.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/viewmodel/intent/InfoBannerInteractionIntent.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/viewmodel/intent/InvoiceClickIntent.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/viewmodel/intent/KeyboardStateChangeIntent.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/viewmodel/intent/SkontoAmountFieldChangeIntent.kt create mode 100644 bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/viewmodel/intent/SkontoDueDateChangeIntent.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 2e6894880..0b096789c 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 @@ -47,7 +46,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 @@ -59,7 +57,6 @@ import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.rememberVectorPainter 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 @@ -69,21 +66,21 @@ 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.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 @@ -101,6 +98,8 @@ import net.gini.android.capture.ui.theme.GiniTheme import net.gini.android.capture.ui.theme.modifier.tabletMaxWidth 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 @@ -155,12 +154,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() }, @@ -188,27 +184,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, @@ -217,9 +197,9 @@ 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) { @@ -228,10 +208,13 @@ private fun ScreenContent( 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() } } @@ -243,7 +226,7 @@ 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, @@ -254,7 +237,7 @@ private fun ScreenContent( @Composable private fun ScreenStateContent( - state: DigitalInvoiceSkontoScreenState, + state: SkontoScreenState, isBottomNavigationBarEnabled: Boolean, onSkontoAmountChange: (BigDecimal) -> Unit, onDueDateChanged: (LocalDate) -> Unit, @@ -268,7 +251,7 @@ private fun ScreenStateContent( screenColorScheme: DigitalInvoiceSkontoScreenColors = DigitalInvoiceSkontoScreenColors.colors() ) { when (state) { - is DigitalInvoiceSkontoScreenState.Ready -> ScreenReadyState( + is SkontoScreenState.Ready -> ScreenReadyState( modifier = modifier, state = state, screenColorScheme = screenColorScheme, @@ -288,7 +271,7 @@ private fun ScreenStateContent( @Composable private fun ScreenReadyState( - state: DigitalInvoiceSkontoScreenState.Ready, + state: SkontoScreenState.Ready, onBackClicked: () -> Unit, onInvoiceClicked: () -> Unit, onHelpClicked: () -> Unit, @@ -530,7 +513,7 @@ private fun SkontoSection( onInfoBannerClicked: () -> Unit, edgeCase: SkontoEdgeCase?, colors: DigitalInvoiceSkontoSectionColors, - skontoAmountValidationError: DigitalInvoiceSkontoScreenState.Ready.SkontoAmountValidationError?, + skontoAmountValidationError: SkontoScreenState.Ready.SkontoAmountValidationError?, modifier: Modifier = Modifier, ) { val dateFormatter = DateTimeFormatter.ofPattern("dd.MM.yyyy") @@ -893,7 +876,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"), 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 8e039a3a4..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 @@ -2,6 +2,14 @@ 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 @@ -9,13 +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(), + ) + } + factory { BackClickIntent() } + factory { InfoBannerInteractionIntent() } + factory { KeyboardStateChangeIntent() } + factory { + SkontoDueDateChangeIntent( + getSkontoRemainingDaysUseCase = get(), + getSkontoEdgeCaseUseCase = get(), + ) + } + factory { + SkontoAmountFieldChangeIntent( digitalInvoiceSkontoAmountValidator = get(), + getSkontoDiscountPercentageUseCase = get(), ) } - factory { DigitalInvoiceSkontoAmountValidator() } + } 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 3cd92003a..000000000 --- a/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/digitalinvoice/skonto/DigitalInvoiceSkontoViewModel.kt +++ /dev/null @@ -1,191 +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.digitalinvoice.skonto.validation.DigitalInvoiceSkontoAmountValidator -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, - private val digitalInvoiceSkontoAmountValidator: DigitalInvoiceSkontoAmountValidator, -) : 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, - skontoAmountValidationError = null, - ) - } - - fun onSkontoAmountFieldChanged(newValue: BigDecimal) = viewModelScope.launch { - val currentState = - stateFlow.value as? DigitalInvoiceSkontoScreenState.Ready ?: return@launch - - val skontoAmountValidationError = digitalInvoiceSkontoAmountValidator( - newValue, - currentState.fullAmount.value - ) - - if (skontoAmountValidationError != null) { - stateFlow.emit( - currentState.copy( - skontoAmount = currentState.skontoAmount, - skontoAmountValidationError = DigitalInvoiceSkontoScreenState.Ready - .SkontoAmountValidationError.SkontoAmountMoreThanFullAmount - ) - ) - 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, - skontoAmountValidationError = null, - ) - ) - } - - - 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 onKeyboardStateChanged(isVisible: Boolean) = viewModelScope.launch { - if (isVisible) return@launch - val currentState = - stateFlow.value as? DigitalInvoiceSkontoScreenState.Ready ?: return@launch - stateFlow.emit( - currentState.copy( - skontoAmountValidationError = null - ) - ) - } - - 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 72% 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 a52fdad83..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,7 +6,7 @@ import net.gini.android.capture.Amount import java.math.BigDecimal import java.time.LocalDate -internal sealed interface DigitalInvoiceSkontoScreenState { +internal sealed interface SkontoScreenState { data class Ready( val isSkontoSectionActive: Boolean, @@ -19,7 +19,7 @@ internal sealed interface DigitalInvoiceSkontoScreenState { val paymentMethod: SkontoData.SkontoPaymentMethod, val edgeCase: SkontoEdgeCase?, val edgeCaseInfoDialogVisible: Boolean, - ) : DigitalInvoiceSkontoScreenState { + ) : SkontoScreenState { sealed interface SkontoAmountValidationError { object SkontoAmountMoreThanFullAmount : SkontoAmountValidationError @@ -27,10 +27,3 @@ internal sealed interface DigitalInvoiceSkontoScreenState { } } -internal sealed interface DigitalInvoiceSkontoSideEffect { - data class OpenInvoiceScreen(val documentId: String, val infoTextLines: List) : - DigitalInvoiceSkontoSideEffect - - object OpenHelpScreen : DigitalInvoiceSkontoSideEffect -} - 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 index f2fdda6c5..14f268638 100644 --- 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 @@ -2,12 +2,12 @@ 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.DigitalInvoiceSkontoScreenState +import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.SkontoScreenState -internal fun DigitalInvoiceSkontoScreenState.Ready.SkontoAmountValidationError.toErrorMessage( +internal fun SkontoScreenState.Ready.SkontoAmountValidationError.toErrorMessage( resources: Resources, ): String = when (this) { - is DigitalInvoiceSkontoScreenState.Ready.SkontoAmountValidationError.SkontoAmountMoreThanFullAmount -> + 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 index 71cd8cda1..2b7ab84d2 100644 --- 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 @@ -1,14 +1,14 @@ package net.gini.android.bank.sdk.capture.digitalinvoice.skonto.validation -import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.DigitalInvoiceSkontoScreenState +import net.gini.android.bank.sdk.capture.digitalinvoice.skonto.SkontoScreenState import java.math.BigDecimal internal class DigitalInvoiceSkontoAmountValidator { operator fun invoke(newSkontoAmount: BigDecimal, fullAmount: BigDecimal) - : DigitalInvoiceSkontoScreenState.Ready.SkontoAmountValidationError? = when { + : SkontoScreenState.Ready.SkontoAmountValidationError? = when { newSkontoAmount > fullAmount -> - DigitalInvoiceSkontoScreenState.Ready.SkontoAmountValidationError.SkontoAmountMoreThanFullAmount + 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/SkontoScreenContent.kt b/bank-sdk/sdk/src/main/java/net/gini/android/bank/sdk/capture/skonto/SkontoScreenContent.kt index bdd7f8f12..f45124d5a 100644 --- 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 @@ -278,7 +278,7 @@ private fun ScreenReadyState( isActive = state.isSkontoSectionActive, onSkontoAmountChange = onDiscountAmountChange, onDueDateChanged = onDueDateChanged, - edgeCase = state.skontoEdgeCase, + edgeCase = state.edgeCase, onInfoBannerClicked = onInfoBannerClicked, discountPercentageFormatter = discountPercentageFormatter, skontoAmountValidationError = state.skontoAmountValidationError, @@ -296,7 +296,7 @@ private fun ScreenReadyState( } if (state.edgeCaseInfoDialogVisible) { - val text = when (state.skontoEdgeCase) { + val text = when (state.edgeCase) { SkontoEdgeCase.PayByCashToday, SkontoEdgeCase.PayByCashOnly -> stringResource(id = R.string.gbs_skonto_section_info_dialog_pay_cash_message) @@ -1024,7 +1024,7 @@ private fun previewState() = SkontoScreenState.Ready( fullAmount = Amount.parse("100:EUR"), totalAmount = Amount.parse("97:EUR"), paymentMethod = SkontoData.SkontoPaymentMethod.PayPal, - skontoEdgeCase = SkontoEdgeCase.PayByCashOnly, + edgeCase = SkontoEdgeCase.PayByCashOnly, edgeCaseInfoDialogVisible = false, savedAmount = Amount.parse("3:EUR"), transactionDialogVisible = true, 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 index 26e1a23fb..6af359a4c 100644 --- 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 @@ -20,7 +20,7 @@ internal sealed interface SkontoScreenState { val totalAmount: Amount, val savedAmount: Amount, val paymentMethod: SkontoData.SkontoPaymentMethod, - val skontoEdgeCase: SkontoEdgeCase?, + val edgeCase: SkontoEdgeCase?, val edgeCaseInfoDialogVisible: Boolean, val transactionDialogVisible: Boolean, ) : SkontoScreenState { 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 index 59c75b450..3ff3d682e 100644 --- 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 @@ -40,7 +40,7 @@ internal class SkontoScreenInitialStateFactory( fullAmount = data.fullAmountToPay, totalAmount = totalAmount, paymentMethod = paymentMethod, - skontoEdgeCase = edgeCase, + edgeCase = edgeCase, edgeCaseInfoDialogVisible = edgeCase != null, savedAmount = savedAmount, transactionDialogVisible = false, 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 index 84721c638..d3de22a7a 100644 --- 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 @@ -18,7 +18,7 @@ internal class SkontoDueDateChangeIntent( state.copy( discountDueDate = newDate, paymentInDays = newPayInDays, - skontoEdgeCase = getSkontoEdgeCaseUseCase.execute( + edgeCase = getSkontoEdgeCaseUseCase.execute( dueDate = newDate, paymentMethod = state.paymentMethod ) From 46364874ab5ace74b74141dcc492a4249c3388ac Mon Sep 17 00:00:00 2001 From: Niko Date: Wed, 20 Nov 2024 15:41:11 +0100 Subject: [PATCH 16/24] feature(bank-sdk): Skonto + RA. Code refactor PP-795 --- .../ui/components/textinput/amount/DecimalFormatter.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 8614b41a8..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 @@ -15,11 +15,11 @@ class DecimalFormatter( ) { fun parseAmount(amount: BigDecimal) = numberFormat.format(amount).trim() - .filter { it != '.' && it != ',' } + .filter { NUMBER_CHARS.contains(it) } .trimStart('0') fun textToDigits(text: String): String = text.trim() - .filter { it != '.' && it != ',' } + .filter { NUMBER_CHARS.contains(it) } .trimStart('0') fun parseDigits(digits: String): BigDecimal = @@ -32,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 From d945049f40c35676af5c8dadba42a6e85cee775d Mon Sep 17 00:00:00 2001 From: Niko Date: Thu, 21 Nov 2024 19:08:01 +0100 Subject: [PATCH 17/24] feature(bank-sdk): Skonto + RA. Code refactor PP-795 --- .../ui/components/textinput/amount/GiniAmountTextInput.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 2c006bbcd..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 @@ -50,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, From 9ca03a90e7711837bbb9f9da46a7a86e8d010dba Mon Sep 17 00:00:00 2001 From: Niko Date: Mon, 2 Dec 2024 11:01:24 +0100 Subject: [PATCH 18/24] feature(bank-sdk): Skonto + RA. Code refactor PP-795 --- .../example-app/src/main/AndroidManifest.xml | 3 +- .../sdk/capture/skonto/SkontoScreenContent.kt | 15 ++++++++- .../validation/SkontoAmountValidator.kt | 4 +-- .../validation/SkontoFullAmountValidator.kt | 4 +-- .../capture/util/compose/ImeListener.kt | 31 +++++++++++++++++++ 5 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 capture-sdk/sdk/src/main/java/net/gini/android/capture/util/compose/ImeListener.kt diff --git a/bank-sdk/example-app/src/main/AndroidManifest.xml b/bank-sdk/example-app/src/main/AndroidManifest.xml index 9347ed635..a6a3cb1ea 100644 --- a/bank-sdk/example-app/src/main/AndroidManifest.xml +++ b/bank-sdk/example-app/src/main/AndroidManifest.xml @@ -50,7 +50,8 @@ + android:launchMode="singleTask" + android:windowSoftInputMode="adjustResize"> 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 index f45124d5a..8db5f546d 100644 --- 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 @@ -8,6 +8,7 @@ import android.widget.FrameLayout import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.focusable @@ -19,8 +20,10 @@ 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.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -87,6 +90,7 @@ 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.rememberImeState import net.gini.android.capture.view.InjectedViewAdapterInstance import org.orbitmvi.orbit.compose.collectAsState import org.orbitmvi.orbit.compose.collectSideEffect @@ -214,7 +218,16 @@ private fun ScreenReadyState( screenColorScheme: SkontoScreenColors = SkontoScreenColors.colors(), ) { val scrollState = rememberScrollState() - Scaffold(modifier = modifier, + val imeState = rememberImeState() + + + LaunchedEffect(key1 = imeState.value) { + if (imeState.value) { + scrollState.animateScrollTo(scrollState.maxValue, tween(300)) + } + } + + Scaffold(modifier = modifier.fillMaxSize().imePadding(), containerColor = screenColorScheme.backgroundColor, topBar = { TopAppBar( 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 index 68dc94403..6b91f6e30 100644 --- 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 @@ -12,13 +12,13 @@ internal class SkontoAmountValidator { newSkontoAmount > fullAmount -> SkontoScreenState.Ready.SkontoAmountValidationError.SkontoAmountMoreThanFullAmount - newSkontoAmount > BigDecimal.valueOf(SKONTO_AMOUNT_LIMIT) -> + newSkontoAmount > BigDecimal(SKONTO_AMOUNT_LIMIT) -> SkontoScreenState.Ready.SkontoAmountValidationError.SkontoAmountLimitExceeded else -> null } companion object { - internal const val SKONTO_AMOUNT_LIMIT = 99_999L + 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 index 0149c8a35..832844a62 100644 --- 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 @@ -9,13 +9,13 @@ internal class SkontoFullAmountValidator { fullAmount: BigDecimal ): SkontoScreenState.Ready.FullAmountValidationError? = when { - fullAmount > BigDecimal.valueOf(SKONTO_AMOUNT_LIMIT) -> + fullAmount > BigDecimal(SKONTO_AMOUNT_LIMIT) -> SkontoScreenState.Ready.FullAmountValidationError.FullAmountLimitExceeded else -> null } companion object { - private const val SKONTO_AMOUNT_LIMIT = 99_999L + private const val SKONTO_AMOUNT_LIMIT = "99999.99" } } 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 +} From 00a13ed0cc98c7b5a5a02b6d0c76a1e1c10b1e3b Mon Sep 17 00:00:00 2001 From: Niko Date: Mon, 2 Dec 2024 11:08:42 +0100 Subject: [PATCH 19/24] feature(bank-sdk): Skonto + RA. Code refactor PP-795 --- .../sdk/capture/skonto/SkontoScreenContent.kt | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) 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 index 8db5f546d..6d9350c08 100644 --- 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 @@ -221,13 +221,13 @@ private fun ScreenReadyState( val imeState = rememberImeState() - LaunchedEffect(key1 = imeState.value) { - if (imeState.value) { - scrollState.animateScrollTo(scrollState.maxValue, tween(300)) - } - } + LaunchedEffect(key1 = imeState.value) { + if (imeState.value) { + scrollState.animateScrollTo(scrollState.maxValue, tween(300)) + } + } - Scaffold(modifier = modifier.fillMaxSize().imePadding(), + Scaffold(modifier = modifier, containerColor = screenColorScheme.backgroundColor, topBar = { TopAppBar( @@ -255,7 +255,9 @@ private fun ScreenReadyState( Column( modifier = Modifier .padding(it) - .verticalScroll(scrollState), + .verticalScroll(scrollState) + .fillMaxSize() + .imePadding(), horizontalAlignment = Alignment.CenterHorizontally, ) { Column( From b9546eee7a568aca24f49d9d82f5abc6d326a6b9 Mon Sep 17 00:00:00 2001 From: Niko Date: Mon, 2 Dec 2024 11:40:50 +0100 Subject: [PATCH 20/24] feature(bank-sdk): Skonto + RA. Code refactor PP-795 --- .../example-app/src/main/AndroidManifest.xml | 3 +-- .../sdk/capture/skonto/SkontoScreenContent.kt | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/bank-sdk/example-app/src/main/AndroidManifest.xml b/bank-sdk/example-app/src/main/AndroidManifest.xml index a6a3cb1ea..9347ed635 100644 --- a/bank-sdk/example-app/src/main/AndroidManifest.xml +++ b/bank-sdk/example-app/src/main/AndroidManifest.xml @@ -50,8 +50,7 @@ + android:launchMode="singleTask"> 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 index 6d9350c08..84da7b892 100644 --- 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 @@ -2,8 +2,10 @@ package net.gini.android.bank.sdk.capture.skonto +import android.app.Activity import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.icu.util.Calendar +import android.util.Log import android.widget.FrameLayout import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility @@ -23,7 +25,6 @@ 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.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -55,6 +56,8 @@ import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource @@ -63,6 +66,8 @@ 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.view.ViewCompat +import androidx.core.view.WindowInsetsCompat 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 @@ -98,6 +103,7 @@ import java.math.BigDecimal import java.time.LocalDate import java.time.format.DateTimeFormatter + @Composable internal fun SkontoScreenContent( isBottomNavigationBarEnabled: Boolean, @@ -219,7 +225,13 @@ private fun ScreenReadyState( ) { val scrollState = rememberScrollState() val imeState = rememberImeState() + val activity = LocalView.current.context as? Activity + val insets = ViewCompat.getRootWindowInsets(activity!!.window.decorView) + //Enjoy your keyboard height + val keyboardHeight = with(LocalDensity.current) { + insets!!.getInsets(WindowInsetsCompat.Type.ime()).bottom.toDp() + } LaunchedEffect(key1 = imeState.value) { if (imeState.value) { @@ -227,6 +239,8 @@ private fun ScreenReadyState( } } + Log.d("AAAAAA", "$keyboardHeight") + Scaffold(modifier = modifier, containerColor = screenColorScheme.backgroundColor, topBar = { @@ -257,7 +271,7 @@ private fun ScreenReadyState( .padding(it) .verticalScroll(scrollState) .fillMaxSize() - .imePadding(), + .padding(bottom = keyboardHeight), horizontalAlignment = Alignment.CenterHorizontally, ) { Column( From 69ac3961867e62f0f756e5dbb0933ec340a31d25 Mon Sep 17 00:00:00 2001 From: Niko Date: Mon, 2 Dec 2024 11:45:21 +0100 Subject: [PATCH 21/24] feature(bank-sdk): Skonto + RA. Code refactor PP-795 --- .../android/bank/sdk/capture/skonto/SkontoScreenContent.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 84da7b892..e3e56e909 100644 --- 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 @@ -233,11 +233,11 @@ private fun ScreenReadyState( insets!!.getInsets(WindowInsetsCompat.Type.ime()).bottom.toDp() } - LaunchedEffect(key1 = imeState.value) { + /* LaunchedEffect(key1 = imeState.value) { if (imeState.value) { scrollState.animateScrollTo(scrollState.maxValue, tween(300)) } - } + }*/ Log.d("AAAAAA", "$keyboardHeight") From f8b7d64a46ba70fe00d416a25b9cadaaa8f407f2 Mon Sep 17 00:00:00 2001 From: Niko Date: Mon, 2 Dec 2024 11:57:15 +0100 Subject: [PATCH 22/24] feature(bank-sdk): Skonto + RA. Code refactor PP-795 --- .../bank/sdk/capture/skonto/SkontoScreenContent.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) 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 index e3e56e909..9246a0f8a 100644 --- 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 @@ -227,17 +227,21 @@ private fun ScreenReadyState( val imeState = rememberImeState() val activity = LocalView.current.context as? Activity val insets = ViewCompat.getRootWindowInsets(activity!!.window.decorView) + val textInputHeightPx = with(LocalDensity.current) { 8.dp.toPx() } //Enjoy your keyboard height val keyboardHeight = with(LocalDensity.current) { insets!!.getInsets(WindowInsetsCompat.Type.ime()).bottom.toDp() } - /* LaunchedEffect(key1 = imeState.value) { + LaunchedEffect(key1 = imeState.value) { if (imeState.value) { - scrollState.animateScrollTo(scrollState.maxValue, tween(300)) + scrollState.animateScrollTo( + scrollState.value + textInputHeightPx.toInt(), + tween(300) + ) } - }*/ + } Log.d("AAAAAA", "$keyboardHeight") From 3be6e182b542b6947d8e3aabf3b58b71d145dc7b Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 3 Dec 2024 11:07:04 +0100 Subject: [PATCH 23/24] feature(bank-sdk): Skonto + RA. Code refactor PP-795 --- .../skonto/DigitalInvoiceSkontoFragment.kt | 6 ++- .../sdk/capture/skonto/SkontoScreenContent.kt | 32 ++------------ .../util/compose/KeybaordPaddingProvider.kt | 44 +++++++++++++++++++ 3 files changed, 52 insertions(+), 30 deletions(-) create mode 100644 capture-sdk/sdk/src/main/java/net/gini/android/capture/util/compose/KeybaordPaddingProvider.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 0b096789c..4a4f559ff 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 @@ -96,6 +96,7 @@ 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 @@ -286,6 +287,8 @@ private fun ScreenReadyState( ) { val scrollState = rememberScrollState() + val keyboardPadding by keyboardPadding(108.dp, scrollState) + Scaffold( modifier = modifier, containerColor = screenColorScheme.backgroundColor, @@ -310,7 +313,8 @@ private fun ScreenReadyState( Column( modifier = Modifier .padding(it) - .verticalScroll(scrollState), + .verticalScroll(scrollState) + .padding(bottom = keyboardPadding), horizontalAlignment = Alignment.CenterHorizontally, ) { Column( 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 index 9246a0f8a..d471482c8 100644 --- 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 @@ -2,15 +2,12 @@ package net.gini.android.bank.sdk.capture.skonto -import android.app.Activity import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.icu.util.Calendar -import android.util.Log import android.widget.FrameLayout import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.focusable @@ -56,8 +53,6 @@ import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource @@ -66,8 +61,6 @@ 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.view.ViewCompat -import androidx.core.view.WindowInsetsCompat 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 @@ -95,7 +88,7 @@ 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.rememberImeState +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 @@ -224,26 +217,7 @@ private fun ScreenReadyState( screenColorScheme: SkontoScreenColors = SkontoScreenColors.colors(), ) { val scrollState = rememberScrollState() - val imeState = rememberImeState() - val activity = LocalView.current.context as? Activity - val insets = ViewCompat.getRootWindowInsets(activity!!.window.decorView) - val textInputHeightPx = with(LocalDensity.current) { 8.dp.toPx() } - - //Enjoy your keyboard height - val keyboardHeight = with(LocalDensity.current) { - insets!!.getInsets(WindowInsetsCompat.Type.ime()).bottom.toDp() - } - - LaunchedEffect(key1 = imeState.value) { - if (imeState.value) { - scrollState.animateScrollTo( - scrollState.value + textInputHeightPx.toInt(), - tween(300) - ) - } - } - - Log.d("AAAAAA", "$keyboardHeight") + val keyboardPadding by keyboardPadding(108.dp, scrollState) Scaffold(modifier = modifier, containerColor = screenColorScheme.backgroundColor, @@ -275,7 +249,7 @@ private fun ScreenReadyState( .padding(it) .verticalScroll(scrollState) .fillMaxSize() - .padding(bottom = keyboardHeight), + .padding(bottom = keyboardPadding), horizontalAlignment = Alignment.CenterHorizontally, ) { Column( 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 +} From 25d04583402136167a79c9b623944ac04b25166a Mon Sep 17 00:00:00 2001 From: Niko Date: Tue, 3 Dec 2024 11:31:13 +0100 Subject: [PATCH 24/24] feature(bank-sdk): Skonto + RA. Code refactor PP-795 --- .../gini/android/bank/sdk/capture/skonto/SkontoScreenContent.kt | 1 - 1 file changed, 1 deletion(-) 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 index 1f7a293f7..f5ac54aa7 100644 --- 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 @@ -48,7 +48,6 @@ 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.graphics.vector.rememberVectorPainter import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource