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:
2026-10-18 15:00:50 +02:00
parent 6159e18198
commit 8c607153c0
96 changed files with 1090 additions and 298 deletions
@@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
@@ -51,16 +52,20 @@ import se.hagstedt.kvitt.design.theme.KvittDimens
import se.hagstedt.kvitt.model.ExpenseId
import se.hagstedt.kvitt.core.design.R as DesignR
/** S-12 Analytics drill-down (design-spec S-12, AC-14.8). [onOpenExpense] opens S-8. */
/**
* S-12 Analytics drill-down (design-spec S-12, AC-14.8). [onOpenExpense] opens S-8. [snackbarHostState] is hosted in
* this screen's scaffold ("Utgiften togs bort" after deleting an expense opened from here, D-071).
*/
@Composable
fun DrillDownScreen(
onOpenExpense: (ExpenseId) -> Unit,
onBack: () -> Unit,
modifier: Modifier = Modifier,
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
viewModel: DrillDownViewModel = hiltViewModel(),
) {
val state by viewModel.state.collectAsStateWithLifecycle()
DrillDownScreen(state, onOpenExpense, onBack, modifier)
DrillDownScreen(state, onOpenExpense, onBack, modifier, snackbarHostState = snackbarHostState)
}
/** The stateless S-12 that previews and screenshot tests render. */
@@ -71,11 +76,13 @@ fun DrillDownScreen(
onBack: () -> Unit,
modifier: Modifier = Modifier,
scrollState: ScrollState = rememberScrollState(),
snackbarHostState: SnackbarHostState? = null,
) {
val title = drillDownTitle(state)
val scrolled by remember(scrollState) { derivedStateOf { scrollState.value > 0 } }
KvittScaffold(
modifier.fillMaxSize(),
snackbarHostState = snackbarHostState,
topBar = { KvittTopAppBar(title, navigation = TopBarNavigation.Back, onNavigation = onBack, scrolled = scrolled) },
) { padding ->
Column(
@@ -70,8 +70,8 @@ class AnalyticsRulesTest {
@Test
fun loadingOnlyBeforeTheFirstSync() {
assertTrue(AnalyticsRules.isFirstSync(SyncSummary(null, 0, 0, clockSkew = false, running = true)))
assertFalse(AnalyticsRules.isFirstSync(SyncSummary("2026-10-17T06:00:00.000Z", 3, 0, clockSkew = false, running = false)))
assertTrue(AnalyticsRules.isFirstSync(SyncSummary(null, 0, 0, clockSkew = false, running = true, lastAttemptFailed = false)))
assertFalse(AnalyticsRules.isFirstSync(SyncSummary("2026-10-17T06:00:00.000Z", 3, 0, clockSkew = false, running = false, lastAttemptFailed = false)))
}
@Test