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:
+9
-6
@@ -7,7 +7,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -16,6 +15,7 @@ import se.hagstedt.kvitt.feature.settings.common.UiMessage
|
||||
import se.hagstedt.kvitt.model.AboutRepository
|
||||
import se.hagstedt.kvitt.model.AboutState
|
||||
import se.hagstedt.kvitt.model.ConnectivityRepository
|
||||
import se.hagstedt.kvitt.model.UpdateCheck
|
||||
import se.hagstedt.kvitt.model.UpdateRepository
|
||||
import se.hagstedt.kvitt.model.UpdateState
|
||||
import se.hagstedt.kvitt.core.design.R as DesignR
|
||||
@@ -37,8 +37,8 @@ class AboutViewModel @Inject constructor(
|
||||
}.stateIn(viewModelScope, SharingStarted.Eagerly, null)
|
||||
|
||||
/**
|
||||
* "Sök efter uppdatering" (online, FR-16): progress in the button; then the banner, or "Du har den senaste
|
||||
* versionen". `check()` reports no failure (§14.2.4), so a check that finds nothing reads as up to date.
|
||||
* "Sök efter uppdatering" (online, FR-16): progress in the button; then the banner (Available), "Du har den
|
||||
* senaste versionen" (UpToDate, AC-16.6) or "Kunde inte nå servern" (Failed).
|
||||
*/
|
||||
fun check() {
|
||||
val s = state.value ?: return
|
||||
@@ -49,9 +49,12 @@ class AboutViewModel @Inject constructor(
|
||||
}
|
||||
local.update { it.copy(checking = true, bannerHidden = false) }
|
||||
viewModelScope.launch {
|
||||
update.check()
|
||||
val found = update.state.first()
|
||||
local.update { it.copy(checking = false, message = if (found is UpdateState.None) UiMessage(R.string.settings_about_up_to_date) else it.message) }
|
||||
val message = when (update.check()) {
|
||||
UpdateCheck.UpToDate -> UiMessage(R.string.settings_about_up_to_date)
|
||||
UpdateCheck.Failed -> UiMessage(DesignR.string.common_could_not_reach_server)
|
||||
is UpdateCheck.Available -> null
|
||||
}
|
||||
local.update { it.copy(checking = false, message = message ?: it.message) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -126,7 +126,7 @@ private fun DeviceRow(item: SegmentedItemScope, device: Device, revocable: Boole
|
||||
val meta = listOfNotNull(
|
||||
stringResource(R.string.settings_devices_paired, listDate(device.pairedAt.take(DATE_LENGTH))),
|
||||
stringResource(R.string.settings_devices_last_seen, relativeTime(device.lastSeenAt)),
|
||||
device.appVersionCode?.let { stringResource(R.string.settings_devices_app_version, it.toString()) },
|
||||
(device.appVersionName ?: device.appVersionCode?.toString())?.let { stringResource(R.string.settings_devices_app_version, it) },
|
||||
).joinToString(dot)
|
||||
Row(
|
||||
Modifier
|
||||
|
||||
+8
-2
@@ -225,11 +225,17 @@ class RecurringEditorViewModel @Inject constructor(
|
||||
local.update { it.copy(triedSave = true) }
|
||||
val s = state.value ?: return
|
||||
if (!s.canSave) return
|
||||
val template = s.form.template(s.original, s.options.me.id, s.options.partner.id, s.options.today)
|
||||
val me = s.options.me.id
|
||||
val partner = s.options.partner.id
|
||||
focus(EditorFocus.None)
|
||||
local.update { it.copy(busy = true) }
|
||||
viewModelScope.launch {
|
||||
val result: OnlineResult<*> = if (s.original == null) recurring.create(template) else recurring.update(template)
|
||||
val original = s.original
|
||||
val result: OnlineResult<*> = if (original == null) {
|
||||
recurring.create(s.form.newTemplate(me, partner, s.options.today))
|
||||
} else {
|
||||
recurring.update(s.form.template(original, me, partner, s.options.today))
|
||||
}
|
||||
local.update { it.copy(busy = false) }
|
||||
when {
|
||||
result is OnlineResult.Ok -> local.update { it.copy(done = true) }
|
||||
|
||||
+15
-4
@@ -7,9 +7,9 @@ import se.hagstedt.kvitt.model.AccountId
|
||||
import se.hagstedt.kvitt.model.CategoryId
|
||||
import se.hagstedt.kvitt.model.ExpenseDraft
|
||||
import se.hagstedt.kvitt.model.ExpenseEditorOptions
|
||||
import se.hagstedt.kvitt.model.NewRecurringTemplate
|
||||
import se.hagstedt.kvitt.model.RecurringTemplate
|
||||
import se.hagstedt.kvitt.model.SplitMode
|
||||
import se.hagstedt.kvitt.model.TemplateId
|
||||
import se.hagstedt.kvitt.model.UserId
|
||||
|
||||
/** Which keypad field has focus (design-spec §3.7: the keypad drives the amount, the exact share and the percents). */
|
||||
@@ -80,16 +80,27 @@ data class RecurringForm(
|
||||
)
|
||||
}
|
||||
|
||||
fun template(original: RecurringTemplate?, me: UserId, partner: UserId, today: String): RecurringTemplate = RecurringTemplate(
|
||||
id = original?.id ?: TemplateId(""),
|
||||
/** A new template for `RecurringRepository.create`, which mints the id (E-28). */
|
||||
fun newTemplate(me: UserId, partner: UserId, today: String): NewRecurringTemplate = NewRecurringTemplate(
|
||||
draft = draft(me, partner, today),
|
||||
dayOfMonth = dayOfMonth ?: 0,
|
||||
startPeriod = startPeriod(today),
|
||||
endPeriod = end,
|
||||
paused = paused,
|
||||
nextOccurrence = original?.nextOccurrence,
|
||||
)
|
||||
|
||||
/** [original] with the form's values, for `RecurringRepository.update`. */
|
||||
fun template(original: RecurringTemplate, me: UserId, partner: UserId, today: String): RecurringTemplate {
|
||||
val fields = newTemplate(me, partner, today)
|
||||
return original.copy(
|
||||
draft = fields.draft,
|
||||
dayOfMonth = fields.dayOfMonth,
|
||||
startPeriod = fields.startPeriod,
|
||||
endPeriod = fields.endPeriod,
|
||||
paused = fields.paused,
|
||||
)
|
||||
}
|
||||
|
||||
/** One keypad key on the focused field; a rejected key leaves the form unchanged. */
|
||||
fun press(key: AmountKey): RecurringForm = when (focus) {
|
||||
EditorFocus.Amount -> amount.press(key)?.let { copy(amount = it) } ?: this
|
||||
|
||||
+4
@@ -104,6 +104,10 @@ class DevicesInvitesAboutScreenshotTest(private val state: String) {
|
||||
about("online", SettingsScenarios.loaded),
|
||||
about("offline", SettingsScenarios.offline),
|
||||
about("check_up_to_date", SettingsScenarios.loaded) { _, vm -> vm.check() },
|
||||
about("check_failed", SettingsScenarios.loaded) { r, vm ->
|
||||
r.checkFails = true
|
||||
vm.check()
|
||||
},
|
||||
about("check_update_found", SettingsScenarios.loaded) { r, vm ->
|
||||
r.afterCheck = UpdateState.Available(SettingsScenarios.update)
|
||||
vm.check()
|
||||
|
||||
Reference in New Issue
Block a user