Android overhaul: shared failure texts, saved-split label and limits in :core:design

* format/Failures.kt: FAILURE_NETWORK and commonFailureText, the §14.3.2 code-to-text map that :feature:account
  (Errors.kt) and :feature:settings (Messages.kt) each spelled out; both keep only their own codes. The "network"
  literals in :feature:onboarding and the two NETWORK constants use FAILURE_NETWORK.
* component/Split.kt: savedSplitLabel, the S-8 and S-16 line ("Kontots standard 50/50", "Egen fördelning 70/30",
  "Exakta belopp", "Allt på dig/Anna") that ExpenseDetailScreen.modeLabel and SyncStatusScreen.lostModeLabel
  duplicated line for line.
* format/Limits.kt: BP_TOTAL (five copies in four features and a literal in :core:design and S-19) and
  DISPLAY_NAME_MAX (three copies in :feature:onboarding and :feature:settings).

Behaviour and goldens unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
2026-10-18 23:04:30 +02:00
parent 8229ed60ea
commit 515b60fc65
24 changed files with 132 additions and 128 deletions
@@ -30,8 +30,6 @@ import androidx.compose.ui.semantics.contentDescription
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import se.hagstedt.kvitt.design.KvittTheme
import se.hagstedt.kvitt.core.design.R as DesignR
import se.hagstedt.kvitt.design.component.KvittScaffold
import se.hagstedt.kvitt.design.component.AmountStyle
import se.hagstedt.kvitt.design.component.AmountTone
import se.hagstedt.kvitt.design.component.BadgeKind
@@ -43,6 +41,7 @@ import se.hagstedt.kvitt.design.component.EmptyState
import se.hagstedt.kvitt.design.component.FactRow
import se.hagstedt.kvitt.design.component.KvittBadge
import se.hagstedt.kvitt.design.component.KvittIconImage
import se.hagstedt.kvitt.design.component.KvittScaffold
import se.hagstedt.kvitt.design.component.KvittTopAppBar
import se.hagstedt.kvitt.design.component.LocalKvittClock
import se.hagstedt.kvitt.design.component.MoneyText
@@ -55,23 +54,21 @@ import se.hagstedt.kvitt.design.component.SyncPendingMarker
import se.hagstedt.kvitt.design.component.Timeline
import se.hagstedt.kvitt.design.component.TopBarNavigation
import se.hagstedt.kvitt.design.component.categoryName
import se.hagstedt.kvitt.design.component.defaultModeLabel
import se.hagstedt.kvitt.design.component.detailDate
import se.hagstedt.kvitt.design.component.kvittLanguage
import se.hagstedt.kvitt.design.component.personName
import se.hagstedt.kvitt.design.component.savedSplitLabel
import se.hagstedt.kvitt.design.component.shareText
import se.hagstedt.kvitt.design.component.spokenMoney
import se.hagstedt.kvitt.design.component.splitModeLabel
import se.hagstedt.kvitt.design.format.DateFormatter
import se.hagstedt.kvitt.design.format.MoneyFormatter
import se.hagstedt.kvitt.design.icon.KvittIcon
import se.hagstedt.kvitt.design.theme.KvittDimens
import se.hagstedt.kvitt.model.AuditAction
import se.hagstedt.kvitt.model.ExpenseDetailState
import se.hagstedt.kvitt.model.ExpenseId
import se.hagstedt.kvitt.model.SplitMode
import se.hagstedt.kvitt.model.User
import kotlin.time.Instant
import se.hagstedt.kvitt.core.design.R as DesignR
/** What S-8 can ask its state holder to do; [ExpenseDetailViewModel] implements it. */
interface ExpenseDetailActions {
@@ -181,7 +178,7 @@ private fun Loaded(detail: ExpenseDetailState, state: ExpenseDetailUiState) {
valueLeading = { PersonAvatar(detail.payer, KvittDimens.avatarChip) },
)
2 -> {
val mode = modeLabel(detail, me, partner)
val mode = savedSplitLabel(detail.mode, detail.shares, me.id, partner.displayName)
val shares = shareText(detail.shares, me, partner, spoken = true)
FactRow(
item,
@@ -257,23 +254,6 @@ private fun DeletedBanner(detail: ExpenseDetailState) {
Banner(BannerKind.Neutral, KvittIcon.Delete, title, body = stringResource(R.string.expense_detail_deleted_body), iconTint = MaterialTheme.colorScheme.error)
}
/** "Kontots standard 50/50" / "Egen fördelning 70/30" / "Exakta belopp" / "Allt på Anna" (design-spec S-8). */
@Composable
private fun modeLabel(detail: ExpenseDetailState, me: User, partner: User): String {
val language = kvittLanguage()
val mineBp = detail.shares.firstOrNull { it.userId == me.id }?.shareBp
val ratio = mineBp?.let { stringResource(DesignR.string.common_ratio, MoneyFormatter.basisPoints(it, language), MoneyFormatter.basisPoints(BP_TOTAL - it, language)) }
return when (detail.mode) {
SplitMode.DEFAULT -> if (ratio != null) stringResource(DesignR.string.editor_split_mode_default, ratio) else defaultModeLabel(HALF_PERCENT)
SplitMode.RATIO -> splitModeLabel(SplitMode.RATIO, "") + (ratio?.let { " $it" } ?: "")
SplitMode.EXACT -> splitModeLabel(SplitMode.EXACT, "")
SplitMode.FULL -> {
val bearer = detail.shares.firstOrNull { it.shareOre > 0 }?.userId
if (bearer == me.id) stringResource(DesignR.string.editor_split_full_on_you) else stringResource(DesignR.string.editor_split_full_on, partner.displayName)
}
}
}
private fun viewerPercent(detail: ExpenseDetailState, me: User): Float {
val total = detail.shares.sumOf { it.shareOre }
val mine = detail.shares.firstOrNull { it.userId == me.id }?.shareOre ?: 0L
@@ -282,6 +262,5 @@ private fun viewerPercent(detail: ExpenseDetailState, me: User): Float {
private const val FACTS = 3
private const val FACTS_WITH_NOTES = 4
private const val BP_TOTAL = 10_000
private const val HALF_PERCENT = 50
private const val FULL_PERCENT = 100f
@@ -22,6 +22,7 @@ import se.hagstedt.kvitt.design.component.AccountChoice
import se.hagstedt.kvitt.design.component.quickRowCategories
import se.hagstedt.kvitt.design.format.AmountInput
import se.hagstedt.kvitt.design.format.AmountKey
import se.hagstedt.kvitt.design.format.BP_TOTAL
import se.hagstedt.kvitt.model.Account
import se.hagstedt.kvitt.model.AccountId
import se.hagstedt.kvitt.model.AccountRepository
@@ -204,7 +205,7 @@ class ExpenseEditorViewModel @Inject constructor(
payer = draft.payer,
mode = draft.mode,
ratioViewer = viewerBp?.let { AmountInput.percent(it.toLong()) } ?: AmountInput(),
ratioPartner = viewerBp?.let { AmountInput.percent(BP_TOTAL - it) } ?: AmountInput(),
ratioPartner = viewerBp?.let { AmountInput.percent((BP_TOTAL - it).toLong()) } ?: AmountInput(),
exactTyped = me,
exactInput = exactMine?.let(AmountInput::of) ?: AmountInput(),
fullOn = draft.fullOn ?: if (draft.payer == me) partner else me,
@@ -523,7 +524,6 @@ class ExpenseEditorViewModel @Inject constructor(
/** design-spec §5.3. */
const val SUGGESTION_DEBOUNCE_MS = 150L
private const val BP_TOTAL = 10_000L
private const val HALF_BP = 5000
private const val PERCENT = 100
private val SPLIT_TARGETS = setOf(KeypadTarget.RatioViewer, KeypadTarget.RatioPartner, KeypadTarget.Exact)