Coverage Summary for Class: TransactionCreationTransferBlockKt (com.georgeci.moneysurfer.feature.transaction.creation)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| TransactionCreationTransferBlockKt |
100%
(1/1)
|
100%
(4/4)
|
56.1%
(37/66)
|
97.5%
(117/120)
|
93.8%
(1335/1424)
|
package com.georgeci.moneysurfer.feature.transaction.creation
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
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.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
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.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.formatter.MoneyFormatter
import com.georgeci.moneysurfer.domain.model.Account
import com.georgeci.moneysurfer.uikit.components.SurferPickerRow
import com.georgeci.moneysurfer.uikit.icons.SurferIcons
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_account_empty
import moneysurfer.feature.transaction.generated.resources.transaction_creation_amount_placeholder
import moneysurfer.feature.transaction.generated.resources.transaction_creation_from_account
import moneysurfer.feature.transaction.generated.resources.transaction_creation_from_label
import moneysurfer.feature.transaction.generated.resources.transaction_creation_rate_hint
import moneysurfer.feature.transaction.generated.resources.transaction_creation_swap_content_description
import moneysurfer.feature.transaction.generated.resources.transaction_creation_to_account
import moneysurfer.feature.transaction.generated.resources.transaction_creation_to_label
import org.jetbrains.compose.resources.stringResource
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun TransferAccountsBlock(
state: TransactionCreationState.Content,
fromAmountState: TextFieldState,
toAmountState: TextFieldState,
onEvent: (TransactionCreationEvent) -> Unit,
) {
val crossCurrency = state.crossCurrency
val fromSymbol = currencySymbol(state.fromAccount?.currencyCode)
val toSymbol = currencySymbol(state.toAccount?.currencyCode)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
TransferLegCard(
label = stringResource(Res.string.transaction_creation_from_label),
currencySymbol = fromSymbol,
amountText = fromAmountState.text.toString(),
amountState = fromAmountState,
account = state.fromAccount,
accountPlaceholder = stringResource(Res.string.transaction_creation_from_account),
onAccountClick = { onEvent(TransactionCreationEvent.OnOpenFromAccountChooser) },
)
state.amountError?.let { error ->
AmountErrorText(error, modifier = Modifier.padding(start = 4.dp))
}
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
Box(
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(AppTheme.materialColors.primary)
.clickable { onEvent(TransactionCreationEvent.OnSwapAccountsClick) },
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = SurferIcons.SwapHoriz,
contentDescription = stringResource(Res.string.transaction_creation_swap_content_description),
tint = AppTheme.materialColors.onPrimary,
modifier = Modifier.size(20.dp),
)
}
}
TransferLegCard(
label = stringResource(Res.string.transaction_creation_to_label),
currencySymbol = toSymbol,
amountText = if (crossCurrency) toAmountState.text.toString() else fromAmountState.text.toString(),
amountState = if (crossCurrency) toAmountState else null,
account = state.toAccount,
accountPlaceholder = stringResource(Res.string.transaction_creation_to_account),
onAccountClick = { onEvent(TransactionCreationEvent.OnOpenToAccountChooser) },
)
state.toAmountError?.let { error ->
AmountErrorText(error, modifier = Modifier.padding(start = 4.dp))
}
if (crossCurrency) {
RateHint(state = state, fromSymbol = fromSymbol, toSymbol = toSymbol)
}
}
}
@Composable
private fun RateHint(
state: TransactionCreationState.Content,
fromSymbol: String,
toSymbol: String,
) {
val from = state.amount.toDoubleOrNull()?.takeIf { it > 0 } ?: return
val to = state.toAmount.toDoubleOrNull()?.takeIf { it > 0 } ?: return
Text(
text = stringResource(
Res.string.transaction_creation_rate_hint,
fromSymbol,
formatRate(to / from),
toSymbol,
),
style = AppTheme.typography.bodySmall,
color = AppTheme.materialColors.onSurfaceVariant,
modifier = Modifier.padding(start = 4.dp, top = 2.dp),
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TransferLegCard(
label: String,
currencySymbol: String,
amountText: String,
/** Non-null makes the leg editable; the receiving leg passes null unless the rate differs. */
amountState: TextFieldState?,
account: Account?,
accountPlaceholder: String,
onAccountClick: () -> Unit,
) {
val shape = AppTheme.shapes.small
Column(
modifier = Modifier
.fillMaxWidth()
.clip(shape)
.background(AppTheme.materialColors.surfaceVariant.copy(alpha = 0.4f))
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = label,
style = AppTheme.typography.labelMedium,
color = AppTheme.materialColors.onSurfaceVariant,
modifier = Modifier.weight(1f),
)
Text(
text = currencySymbol,
style = AppTheme.typography.headlineMedium,
color = AppTheme.materialColors.onSurfaceVariant,
)
Spacer(Modifier.width(4.dp))
if (amountState != null) {
BasicTextField(
state = amountState,
inputTransformation = AmountInputTransformation,
lineLimits = TextFieldLineLimits.SingleLine,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
textStyle = LocalTextStyle.current.merge(AppTheme.typography.headlineLarge).copy(
color = AppTheme.materialColors.onSurface,
textAlign = TextAlign.End,
fontWeight = FontWeight.Medium,
),
cursorBrush = SolidColor(AppTheme.materialColors.primary),
)
} else {
Text(
text = amountText.ifEmpty { stringResource(Res.string.transaction_creation_amount_placeholder) },
style = AppTheme.typography.headlineLarge,
color = AppTheme.materialColors.onSurface,
fontWeight = FontWeight.Medium,
)
}
}
SurferPickerRow(
label = accountPlaceholder,
value = account?.let { acc ->
val balance = MoneyFormatter.format(acc.balance, acc.currencyCode)
"${acc.name} ยท $balance"
} ?: stringResource(Res.string.transaction_creation_account_empty),
icon = SurferIcons.CreditCard,
onClick = onAccountClick,
)
}
}
private const val RATE_DECIMAL_SCALE = 10_000.0
private fun formatRate(rate: Double): String {
val rounded = (rate * RATE_DECIMAL_SCALE).toLong() / RATE_DECIMAL_SCALE
val asLong = rounded.toLong()
return if (rounded == asLong.toDouble()) {
asLong.toString()
} else {
rounded.toString()
}
}