Coverage Summary for Class: TransactionCreationAmountHeroKt (com.georgeci.moneysurfer.feature.transaction.creation)

Class Method, % Branch, % Line, % Instruction, %
TransactionCreationAmountHeroKt 100% (4/4) 69.4% (25/36) 93.2% (68/73) 95.6% (735/769)
TransactionCreationAmountHeroKt$AmountHero$1$1$1 100% (1/1) 50% (4/8) 100% (10/10) 97.3% (146/150)
TransactionCreationAmountHeroKt$WhenMappings
Total 100% (5/5) 65.9% (29/44) 94% (78/83) 95.9% (881/919)


 package com.georgeci.moneysurfer.feature.transaction.creation
 
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 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.width
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.foundation.text.BasicTextField
 import androidx.compose.foundation.text.KeyboardOptions
 import androidx.compose.foundation.text.input.TextFieldLineLimits
 import androidx.compose.foundation.text.input.TextFieldState
 import androidx.compose.material3.LocalTextStyle
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.graphics.SolidColor
 import androidx.compose.ui.graphics.compositeOver
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.input.KeyboardType
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.dp
 import com.georgeci.moneysurfer.domain.primitives.CurrencyCode
 import com.georgeci.moneysurfer.uikit.theme.AppTheme
 import com.georgeci.moneysurfer.utils.AmountInputTransformation
 import moneysurfer.feature.transaction.generated.resources.Res
 import moneysurfer.feature.transaction.generated.resources.transaction_creation_amount_error_format
 import moneysurfer.feature.transaction.generated.resources.transaction_creation_amount_error_zero
 import moneysurfer.feature.transaction.generated.resources.transaction_creation_amount_placeholder
 import moneysurfer.feature.transaction.generated.resources.transaction_creation_expense
 import moneysurfer.feature.transaction.generated.resources.transaction_creation_income
 import moneysurfer.feature.transaction.generated.resources.transaction_creation_transfer
 import org.jetbrains.compose.resources.StringResource
 import org.jetbrains.compose.resources.stringResource
 
 /** Opacity of the type pill's tint wash. See [TypePill] for why it is composited, not blended. */
 private const val TYPE_PILL_WASH_ALPHA = 0.18f
 
 @Composable
 internal fun AmountHero(
     type: TransactionTypeUi,
     currencySymbol: String,
     amountState: TextFieldState,
     error: TransactionAmountError?,
 ) {
     Column(
         modifier = Modifier
             .fillMaxWidth()
             .padding(horizontal = AppTheme.spacing.large),
         horizontalAlignment = Alignment.CenterHorizontally,
     ) {
         TypePill(type = type)
         Spacer(Modifier.height(AppTheme.spacing.xSmall))
         Row(verticalAlignment = Alignment.Bottom) {
             Text(
                 text = currencySymbol,
                 style = AppTheme.typography.displayMedium,
                 color = AppTheme.materialColors.onSurfaceVariant,
             )
             Spacer(Modifier.width(4.dp))
             val text = amountState.text.toString()
             val placeholderText = stringResource(Res.string.transaction_creation_amount_placeholder)
             BasicTextField(
                 state = amountState,
                 modifier = Modifier.testTag(TransactionCreationTestTags.Amount),
                 inputTransformation = AmountInputTransformation,
                 lineLimits = TextFieldLineLimits.SingleLine,
                 keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
                 textStyle = LocalTextStyle.current.merge(AppTheme.typography.displayLarge).copy(
                     color = AppTheme.materialColors.onSurface,
                     textAlign = TextAlign.Start,
                     fontWeight = FontWeight.Medium,
                 ),
                 cursorBrush = SolidColor(AppTheme.materialColors.primary),
                 decorator = { innerTextField ->
                     Box {
                         if (text.isEmpty()) {
                             Text(
                                 text = placeholderText,
                                 style = AppTheme.typography.displayLarge,
                                 color = AppTheme.materialColors.onSurfaceVariant,
                             )
                         }
                         innerTextField()
                     }
                 },
             )
         }
         if (error != null) {
             Spacer(Modifier.height(AppTheme.spacing.xSmall))
             AmountErrorText(error)
         }
     }
 }
 
 @Composable
 internal fun AmountErrorText(
     error: TransactionAmountError,
     modifier: Modifier = Modifier,
 ) {
     val messageRes: StringResource = when (error) {
         TransactionAmountError.INVALID_FORMAT -> Res.string.transaction_creation_amount_error_format
         TransactionAmountError.NOT_POSITIVE -> Res.string.transaction_creation_amount_error_zero
     }
     Text(
         text = stringResource(messageRes),
         style = AppTheme.typography.bodySmall,
         color = AppTheme.materialColors.error,
         modifier = modifier,
     )
 }
 
 @Composable
 private fun TypePill(type: TransactionTypeUi) {
     val tint = typeTint(type)
     val labelRes = when (type) {
         TransactionTypeUi.Expense -> Res.string.transaction_creation_expense
         TransactionTypeUi.Income -> Res.string.transaction_creation_income
         TransactionTypeUi.Transfer -> Res.string.transaction_creation_transfer
     }
     // Composited over `surface` rather than left translucent: the label is `tint` on a wash
     // of the same tint, so the contrast ratio depends entirely on what shows through. Pinning
     // the wash to the scaffold's own colour keeps the pair at a known ≥4.5:1 even if the pill
     // is later moved onto a card.
     val wash = tint.copy(alpha = TYPE_PILL_WASH_ALPHA).compositeOver(AppTheme.materialColors.surface)
     Row(
         modifier = Modifier
             .clip(RoundedCornerShape(percent = 50))
             .background(wash)
             .padding(horizontal = 14.dp, vertical = 4.dp),
         verticalAlignment = Alignment.CenterVertically,
     ) {
         Text(
             text = stringResource(labelRes).uppercase(),
             style = AppTheme.typography.labelMedium,
             color = tint,
         )
     }
 }
 
 internal fun currencySymbol(code: CurrencyCode?): String {
     val value = code?.value ?: return "$"
     return when (value) {
         "EUR" -> "€"
         "GBP" -> "£"
         "PLN" -> "zł"
         "JPY" -> "¥"
         "RUB" -> "₽"
         else -> "$"
     }
 }