M12e: contract pass (pair invite kind, admin name in invite links, sync failed flag, rejection actor, device version name, template id, update check result, untouched accounts, D-027 re-stamp, PushEvent in :core:model, snackbar hosts)
Server: E-3 invite_kind; E-12 link ?from=<admin name>, E-4 passes it into kvitt://invite; X-Kvitt-App-Version-Name stored in devices.app_version_name (migration 0003, schema 3); OpenAPI and examples follow. Phone: :core:model, :core:fakes, :data, features and :app updated together; goldens re-recorded for S-20 (version name) and new S-1/S-22 states. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
+19
-6
@@ -14,11 +14,13 @@ import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
@@ -29,6 +31,7 @@ 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
|
||||
@@ -80,22 +83,23 @@ interface ExpenseDetailActions {
|
||||
|
||||
/**
|
||||
* S-8, the expense detail with its timeline (design-spec §4 S-8; SPEC AC-8.11, AC-12.4, AC-12.5). [onEdit] opens S-7
|
||||
* in edit mode; [onDeleted] goes back with the snackbar text "Utgiften togs bort".
|
||||
* in edit mode; [onDeleted] goes back, where "Utgiften togs bort" (`snackbar_expense_deleted`) is shown, as S-10 does.
|
||||
* [snackbarHostState] is hosted in this screen, so `:app` can show a result snackbar here (D-071).
|
||||
*/
|
||||
@Composable
|
||||
fun ExpenseDetailScreen(
|
||||
onBack: () -> Unit,
|
||||
onEdit: (ExpenseId) -> Unit,
|
||||
onDeleted: (String) -> Unit,
|
||||
onDeleted: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||
viewModel: ExpenseDetailViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
val deleted = stringResource(DesignR.string.snackbar_expense_deleted)
|
||||
LaunchedEffect(viewModel) {
|
||||
viewModel.eventFlow.collect { event -> if (event == DetailEvent.Deleted) onDeleted(deleted) }
|
||||
viewModel.eventFlow.collect { event -> if (event == DetailEvent.Deleted) onDeleted() }
|
||||
}
|
||||
ExpenseDetailContent(state, viewModel, onBack, onEdit, modifier)
|
||||
ExpenseDetailContent(state, viewModel, onBack, onEdit, modifier, snackbarHostState)
|
||||
}
|
||||
|
||||
/** The stateless S-8 layout. */
|
||||
@@ -106,10 +110,19 @@ fun ExpenseDetailContent(
|
||||
onBack: () -> Unit,
|
||||
onEdit: (ExpenseId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
snackbarHostState: SnackbarHostState? = null,
|
||||
) {
|
||||
// The scaffold only hosts the snackbar; the body keeps its own system-bar padding.
|
||||
KvittScaffold(modifier.fillMaxSize(), snackbarHostState = snackbarHostState) { _ ->
|
||||
DetailSurface(state, actions, onBack, onEdit)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DetailSurface(state: ExpenseDetailUiState, actions: ExpenseDetailActions, onBack: () -> Unit, onEdit: (ExpenseId) -> Unit) {
|
||||
val detail = state.detail
|
||||
val me = state.me
|
||||
Surface(modifier.fillMaxSize(), color = MaterialTheme.colorScheme.surface) {
|
||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.surface) {
|
||||
if (me == null || state.loading) return@Surface
|
||||
ProvideHousehold(me, state.partner) {
|
||||
Column(Modifier.fillMaxSize().statusBarsPadding().navigationBarsPadding()) {
|
||||
|
||||
+17
-10
@@ -1,6 +1,7 @@
|
||||
package se.hagstedt.kvitt.feature.expense
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -123,28 +124,26 @@ interface ExpenseEditorActions {
|
||||
}
|
||||
|
||||
/**
|
||||
* S-7, the full-screen expense editor (design-spec §4 S-7, §5). [onSaved] gets the saved id and the snackbar text for
|
||||
* the opener ("Utgiften sparades" / offline wording for a new expense, null after an edit: back to S-8 without one);
|
||||
* [onDeleted] gets "Utgiften togs bort" and goes back past S-8 to its opener.
|
||||
* S-7, the full-screen expense editor (design-spec §4 S-7, §5). [onSaved] gets the saved id and the snackbar string
|
||||
* resource for the opener (`snackbar_expense_saved` / `snackbar_expense_saved_offline` for a new expense, null after an
|
||||
* edit: back to S-8 without one); [onDeleted] goes back past S-8 to its opener, which shows `snackbar_expense_deleted`.
|
||||
* `:app` resolves the resource while the snackbar shows, as for S-9 and S-10.
|
||||
*/
|
||||
@Composable
|
||||
fun ExpenseEditorScreen(
|
||||
onClose: () -> Unit,
|
||||
onSaved: (ExpenseId, String?) -> Unit,
|
||||
onDeleted: (String) -> Unit,
|
||||
onSaved: (ExpenseId, Int?) -> Unit,
|
||||
onDeleted: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: ExpenseEditorViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||
val descriptionFocus = remember { FocusRequester() }
|
||||
val saved = stringResource(DesignR.string.snackbar_expense_saved)
|
||||
val savedOffline = stringResource(DesignR.string.snackbar_expense_saved_offline)
|
||||
val deleted = stringResource(DesignR.string.snackbar_expense_deleted)
|
||||
LaunchedEffect(viewModel) {
|
||||
viewModel.eventFlow.collect { event ->
|
||||
when (event) {
|
||||
is EditorEvent.Saved -> onSaved(event.id, if (!event.isNew) null else if (event.offline) savedOffline else saved)
|
||||
EditorEvent.Deleted -> onDeleted(deleted)
|
||||
is EditorEvent.Saved -> onSaved(event.id, savedSnackbar(event.isNew, event.offline))
|
||||
EditorEvent.Deleted -> onDeleted()
|
||||
EditorEvent.Closed -> onClose()
|
||||
EditorEvent.FocusDescription -> descriptionFocus.requestFocus()
|
||||
}
|
||||
@@ -155,6 +154,14 @@ fun ExpenseEditorScreen(
|
||||
ExpenseEditorContent(state, viewModel, modifier, descriptionFocus)
|
||||
}
|
||||
|
||||
/** The snackbar after a save: none after an edit; for a new expense the online or offline wording. */
|
||||
@StringRes
|
||||
internal fun savedSnackbar(isNew: Boolean, offline: Boolean): Int? = when {
|
||||
!isNew -> null
|
||||
offline -> DesignR.string.snackbar_expense_saved_offline
|
||||
else -> DesignR.string.snackbar_expense_saved
|
||||
}
|
||||
|
||||
/** The stateless S-7 layout: app bar, scrolling fields, the keypad, and the overlay of [ExpenseEditorState.overlay]. */
|
||||
@Composable
|
||||
fun ExpenseEditorContent(
|
||||
|
||||
Reference in New Issue
Block a user