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
@@ -23,11 +23,10 @@ fun homeState(scenario: FakeScenario, act: (HomeViewModel) -> Unit = {}): HomeUi
vm.ui.value
}
/** Sync status with [scenario]; [failed] stands in for the missing `SyncSummary` field (see the M7b report). */
fun syncState(scenario: FakeScenario, failed: Boolean = false): SyncStatusUi = withMain {
/** Sync status with [scenario]. */
fun syncState(scenario: FakeScenario): SyncStatusUi = withMain {
val fakes = FakeRepositories(scenario)
val ui = SyncStatusViewModel(fakes.sync, fakes.connectivity, fakes.session).ui.value
if (failed && ui is SyncStatusUi.Ready) ui.copy(failed = true) else ui
SyncStatusViewModel(fakes.sync, fakes.connectivity, fakes.session).ui.value
}
/** Every state of S-4, S-16 and S-17 (design-spec §4 and PLAN-M7b §5) × light/dark × sv/en. */
@@ -54,7 +53,7 @@ val homeStates: List<ScreenState> = listOf(
sync("S-16-sync-rejections", HomeScenarios.allRejections),
sync("S-16-sync-clock", HomeScenarios.clockSkew),
sync("S-16-sync-running", HomeScenarios.syncing),
sync("S-16-sync-error", HomeScenarios.pending, failed = true),
sync("S-16-sync-error", HomeScenarios.pending.copy(lastAttemptFailed = true)),
sync("S-16-sync-offline", HomeScenarios.syncOffline),
sync("S-16-sync-long", HomeScenarios.longSync, largeFont = true),
)
@@ -64,8 +63,8 @@ private fun home(name: String, scenario: FakeScenario, largeFont: Boolean = fals
return ScreenState(name, largeFont) { HomeContent(ui, HomeActions()) }
}
private fun sync(name: String, scenario: FakeScenario, largeFont: Boolean = false, failed: Boolean = false): ScreenState {
val ui = syncState(scenario, failed)
private fun sync(name: String, scenario: FakeScenario, largeFont: Boolean = false): ScreenState {
val ui = syncState(scenario)
return ScreenState(name, largeFont) { SyncStatusContent(ui, SyncStatusActions()) }
}
@@ -12,6 +12,7 @@ import se.hagstedt.kvitt.fakes.home.HomeScenarios
import se.hagstedt.kvitt.model.DefaultCategoryKey
import se.hagstedt.kvitt.model.SyncRejectionReason
import se.hagstedt.kvitt.model.UpdateRepository
import se.hagstedt.kvitt.model.UpdateCheck
import se.hagstedt.kvitt.model.UpdateState
/** Records `install()` calls; the update state is set by the test (no mocking library, CLAUDE.md). */
@@ -19,7 +20,7 @@ private class RecordingUpdates(initial: UpdateState) : UpdateRepository {
val flow = MutableStateFlow(initial)
var installs = 0
override val state: Flow<UpdateState> = flow
override suspend fun check() = Unit
override suspend fun check(): UpdateCheck = UpdateCheck.UpToDate
override suspend fun install() {
installs += 1
}