feat(widget): per-residence widget configuration — closes #6 #10
Reference in New Issue
Block a user
Delete Branch "feat/6-widget-residence-picker"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #6.
Users with multiple residences can now pick which one a given home-screen widget shows tasks for, on both iOS and Android. Pinning two widgets — one per house — lets each surface tasks for only that residence.
Single-home users and anyone who leaves the picker untouched continue to see tasks across every residence (the prior default), so this is non-breaking.
iOS
ConfigurationAppIntent@Parameterof typeWidgetResidenceEntityWidgetResidenceEntity+WidgetResidenceEntityQueryAppEntityreading from the App Group sidecarWidgetDataManagersaveResidences(from:)writeswidget_residences.json;clearCache()clears it on sign-out; purefilterTasks(_:forResidenceId:)helperWidgetDataManager.WidgetTask+CacheManager.CustomTaskresidence_idCodable key — legacy JSON still decodesDataManagerObservablemyResidencesupdate intoWidgetDataManager.saveResidences(from:)CacheManager.getUpcomingTasks(forResidenceId:)+Provider.timeline/snapshotconfiguration.residence?.intIdThe picker is the standard
AppIntentsconfiguration sheet (long-press the widget → Edit Widget → Residence).Android
WidgetConfigActivityComponentActivityhosting the residence-picker UIWidgetDataStorewidget_residences_jsonkey + per-instancewidget_residence_id_<appWidgetId>keysWidgetDataRepositorysaveResidences/loadResidences,saveResidenceIdFor/loadResidenceIdFor/clearResidenceIdFor,loadTasksForResidence,loadTasksForWidget(appWidgetId),computeStatsFromTasks(_),Filter.filterTasksForResidence(_, _)WidgetTaskDtoresidenceIdWidgetResidenceDtoWidgetRefreshWorker/DefaultWidgetRefreshDataSourcemyResidencesalongside tasks/tier on each refresh, write the sidecar (best-effort; non-fatal on failure)HoneyDue{Small,Medium,Large}Widget.provideGlanceappWidgetIdviaGlanceAppWidgetManager(context).getAppWidgetId(id)and callloadTasksForWidget(appWidgetId); large widget usescomputeStatsFromTasks(tasks)so its stat tiles reflect the scoped listHoneyDue{Small,Medium,Large}WidgetReceiver.onDeletedAndroidManifest.xmlWidgetConfigActivitywith theAPPWIDGET_CONFIGUREactionhoneydue_{small,medium,large}_widget_info.xmlandroid:configure="com.tt.honeyDue.widget.WidgetConfigActivity"The picker launches automatically when the user pins a new tile and is reachable again via "Edit Widget" on any pinned tile.
Migration / safety
residence_idfield is optional on both platforms' Codable surfaces. Older app builds wrote widget cache without it; those tasks pass through the filter for unscoped widgets (preserves the legacy "all residences" view) and are hidden in scoped ones (we'd rather omit than misattribute).clearAll()sweepswidget_residence_id_<n>keys by prefix on logout.onDeletedpurges the key for a single removed tile.WidgetTask.initandCustomTask.initgained custom inits withresidenceId: Int? = nildefault so existing#Previewliterals andTaskMetricsTestscontinue to compile.Tests
iOS — new
iosApp/HoneyDueTests/WidgetResidenceFilterTests.swift(5 cases): nil-passthrough, matching-id, no-match, missing-residence-on-task in scoped widget, order preservation.Android — new
composeApp/src/androidUnitTest/kotlin/com/tt/honeyDue/widget/WidgetResidenceFilterTest.kt(9 cases): the same 5 filter contracts, plus DataStore round-trip for the per-appWidgetIdkey, end-to-endloadTasksForWidgetwith multiple configured tiles,clearResidenceIdForon tile removal, andsaveResidences/loadResidencesround-trip.Mirrors the iOS implementation. Adds a Glance configuration activity that launches when the user pins a new honeyDue widget tile and again on "Edit Widget", lets them pick one of their residences (or "All residences"), and persists the choice per-`appWidgetId`. Each tile's `provideGlance` resolves its own scope and filters tasks (and stats, on the large widget) accordingly. Pieces: - `WidgetConfigActivity` — Compose `ComponentActivity` hosting the residence-picker UI; reads the persisted residences sidecar, reads any prior scope for the current `appWidgetId`, writes the new selection on Save, and re-renders every widget tile. - `WidgetDataStore` — new `widget_residences_json` key + a per-instance `widget_residence_id_<appWidgetId>` key. `clearAll()` sweeps the per-instance keys by prefix so logout doesn't leave dangling state. - `WidgetDataRepository`: * `saveResidences(_)` / `loadResidences()` for the picker. * `saveResidenceIdFor(appWidgetId, residenceId)` / `loadResidenceIdFor(appWidgetId)` / `clearResidenceIdFor(appWidgetId)` for per-tile scope. * `loadTasksForResidence(residenceId)` and the `appWidgetId`-driven `loadTasksForWidget(appWidgetId)`. * `computeStatsFromTasks(tasks)` so the large widget's tiles reflect only the scoped task list (instead of the whole cache). * Pure `Filter.filterTasksForResidence(_, _)` on the companion object — easy to exercise from unit tests. - `WidgetTaskDto` already carries `residenceId`. New `WidgetResidenceDto` added (id + name) — JSON-persisted via the sidecar. - `WidgetRefreshWorker` / `DefaultWidgetRefreshDataSource` — pull `myResidences` alongside tasks/tier on each refresh and write the sidecar (best-effort; non-fatal if the call fails). - `HoneyDue{Small,Medium,Large}Widget.provideGlance` — resolve `appWidgetId` via `GlanceAppWidgetManager(context).getAppWidgetId(id)` and call `loadTasksForWidget(appWidgetId)`. - `HoneyDue{Small,Medium,Large}WidgetReceiver.onDeleted` — purge the per-instance residence scope key when the tile is removed. - Manifest: register the configure activity with the `APPWIDGET_CONFIGURE` action. - `honeydue_{small,medium,large}_widget_info.xml` — declare `android:configure="com.tt.honeyDue.widget.WidgetConfigActivity"`. Migration / safety: - A tile that's never been through the picker has no residence id saved → `loadTasksForWidget` returns every task (legacy "All residences" behaviour). Existing tiles keep working without the user touching anything. - The picker handles an empty residences list (signed-out / first install before background refresh) with an explicit helper message pointing at the main app. Tests: new `WidgetResidenceFilterTest` (commonTest-style under `androidUnitTest`, 9 cases). All green. $ ./gradlew :composeApp:testDebugUnitTest \\ --tests "com.tt.honeyDue.widget.WidgetResidenceFilterTest" BUILD SUCCESSFUL $ ./gradlew :composeApp:assembleDebug BUILD SUCCESSFUL Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>feat(widget): per-residence widget configuration (iOS) — #6to feat(widget): per-residence widget configuration — closes #6