3 Commits

Author SHA1 Message Date
Trey t f5f02145a2 test: add accessibility identifiers along the onboarding-to-residence-detail path
Scaffolding for the gitea#2 regression XCUITest. No user-visible
change — pure metadata for UI automation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 09:33:47 -05:00
Trey t cb4806b423 plan: task cache unification (closes gitea#2)
Fix the bug where tasks created during onboarding don't appear on
the Residence Detail screen until app restart. Root cause:
DataManager.updateTask is a no-op when both _allTasks is null AND
_tasksByResidence[residenceId] is empty — the case after a fresh
register-then-bulk-create flow.

Approach: collapse the dual cache into a single source of truth
(_allTasks). Residence detail observes it directly and filters by
residenceId in-memory. After mutations, force-refresh _allTasks from
the server (one round-trip eliminates a class of bugs).

Plan covers 14 tasks across 4 phases plus a regression XCUITest
that captures the user-visible bug end-to-end.
2026-04-25 09:25:04 -05:00
Trey t 6bfe058050 iOS: complete bundle ID + team ID migration to com.myhoneydue.*
Carries the rebrand from the backend (APPLE_CLIENT_ID, APNS_TOPIC) all
the way through the iOS targets:

- All target PRODUCT_BUNDLE_IDENTIFIERs: com.tt.honeyDue.* → com.myhoneydue.honeyDue.*
- DEVELOPMENT_TEAM: V3PF3M6B6U → X86BR9WTLD (across every target)
- APP_GROUP_IDENTIFIER: group.com.tt.honeyDue.* → group.com.myhoneydue.honeyDue.*
- BGTaskSchedulerPermittedIdentifiers + BackgroundTaskManager constant
- KeychainHelper service identifier
- StoreKit fallback product IDs + Info.plist IAP product ID keys
- ExportOptions.plist teamID
- NSCamera / NSPhotoLibrary usage descriptions reworded
- Onboarding suggestion strings reworked (new %lld%% match copy,
  dropped old "Great match" / "Good match" / "Generating suggestions"
  strings — replaced by relevance-percentage labels)
- xctestplan + settings.local.json housekeeping

App-group rename means UserDefaults / shared-container data written to
the old group ID is abandoned. Acceptable since this is pre-launch.
2026-04-25 09:24:40 -05:00
19 changed files with 1029 additions and 81 deletions
+2 -1
View File
@@ -18,7 +18,8 @@
"Bash(ps:*)", "Bash(ps:*)",
"Bash(stdbuf:*)", "Bash(stdbuf:*)",
"Bash(sysctl:*)", "Bash(sysctl:*)",
"Bash(tee:*)" "Bash(tee:*)",
"Bash(codesign -d --entitlements :- /Users/treyt/Library/Developer/Xcode/DerivedData/honeyDue-buvczbpttcfkxxcmxbnqkqrmujyh/Build/Products/Debug-iphonesimulator/honeyDue.app)"
] ]
} }
} }
@@ -0,0 +1,919 @@
# Task Cache Unification Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Make `_allTasks` the single source of truth for tasks; collapse `_tasksByResidence` into a derived view. Fix the bug where tasks created during onboarding don't appear on the Residence Detail screen until app restart (Gitea issue #2).
**Architecture:** The current code has two parallel task caches that must be kept in sync (`_allTasks` for the kanban tab, `_tasksByResidence` per-residence for the residence detail screen). `DataManager.updateTask` is a no-op when either cache is empty, so post-`bulkCreateTasks` the new tasks live only on the server until something forces a fetch. After this change there is exactly one cache (`_allTasks`); residence detail screens observe it and apply an in-memory filter by `residenceId`. Mutations (`createTask`, `bulkCreateTasks`) force a refresh of `_allTasks` from the server to guarantee freshness with one round-trip instead of relying on conditional branches that silently skip.
**Tech Stack:** Kotlin Multiplatform (commonMain), Ktor client, kotlinx.serialization, Combine bridge to SwiftUI iOS, Compose StateFlow on Android. Test framework: `kotlin.test` in commonTest.
**Affected files:**
- `composeApp/src/commonMain/kotlin/com/tt/honeyDue/data/DataManager.kt` — remove `_tasksByResidence`, simplify `updateTask`/`removeTask`, add upsert behavior
- `composeApp/src/commonMain/kotlin/com/tt/honeyDue/network/APILayer.kt` — change `bulkCreateTasks`, `createTask`, `getTasksByResidence`
- `composeApp/src/commonMain/kotlin/com/tt/honeyDue/viewmodel/ResidenceViewModel.kt` — feed `_residenceTasksState` from a `combine(allTasks, residenceId)` flow
- `composeApp/src/commonTest/kotlin/com/tt/honeyDue/data/DataManagerTaskCacheTest.kt` (new) — cache behavior tests
- `iosApp/iosApp/Task/TaskViewModel.swift` — drop `$tasksByResidence` sink, filter `$allTasks` when residence-scoped
- `iosApp/iosApp/Data/DataManagerObservable.swift` — drop `tasksByResidence` `@Published` and its `for await` task
**Out of scope (do not touch):**
- Backend Go API — `/api/tasks/by-residence/:id/` endpoint stays in place untouched (might still be used by web admin)
- Android `ResidenceDetailScreen.kt` — the screen contract (`residenceViewModel.residenceTasksState`) is preserved; only the VM internals change
- Disk persistence schema — kotlinx.serialization is configured with `ignoreUnknownKeys` for forward/backward compat (verified in Task 11)
---
## Pre-flight
### Task 0: Verify clean state and baseline
**Files:** none (read only)
**Step 1: Confirm working tree is clean (or only the expected exception)**
Run: `git -C /Users/treyt/Desktop/code/honeyDue/honeyDueKMP status --short`
Expected: empty, **or** the only line is `M composeApp/src/commonMain/kotlin/com/tt/honeyDue/network/ApiConfig.kt`. That single file is intentionally on `Environment.LOCAL` for the duration of this work — it stays uncommitted and gets flipped back to `Environment.PROD` in Task 11 Step 5. If anything else shows up, stop and ask the user.
**Step 2: Confirm we're on a feature branch (not master)**
Run: `git -C /Users/treyt/Desktop/code/honeyDue/honeyDueKMP rev-parse --abbrev-ref HEAD`
Expected: NOT `master`. If `master`, run `git checkout -b fix/task-cache-unification` before continuing.
**Step 3: Run the existing commonTest baseline so we know what currently passes**
Run: `./gradlew :composeApp:testDebugUnitTest`
Expected: BUILD SUCCESSFUL. Note the count — every later run must keep ≥ this count of green tests.
**Step 4: Build iOS to confirm starting point compiles**
Run: `xcodebuild -project iosApp/honeyDue.xcodeproj -scheme iosApp -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 17' build 2>&1 | tail -20`
Expected: `** BUILD SUCCEEDED **`
No commit — this is verification only.
### Task 0.5: Failing regression XCUITest — reproduce the bug end-to-end
**Goal:** Write a UI test that drives the exact onboarding-to-residence-detail flow from gitea#2 and asserts that tasks appear on the residence detail screen without an app restart. Run it now — it MUST fail. The Phase 1-3 fixes will make it pass; Task 12 re-runs it as the final gate.
**Why before the unit tests:** The unit tests in Phase 1 catch the bug at the cache layer, but the *user-facing* bug is "I tap my residence and see 'no tasks'". A passing UI test is the only thing that proves the user experience is actually fixed. Writing it once up front + running it once at the end is cheaper than running a full UI cycle every iteration.
**Files:**
- Create: `iosApp/HoneyDueUITests/Suite11_TaskCacheRegressionTests.swift`
- Maybe modify: `iosApp/HoneyDueUITests/AccessibilityIdentifiers.swift` (only if missing IDs are needed — see Step 2)
- Maybe modify: SwiftUI views in `iosApp/iosApp/` (only if the view layer is missing accessibility identifiers — see Step 2)
**Pre-requisites already satisfied by Task 0 setup:**
- iOS app is on `Environment.LOCAL`
- Docker stack is up and healthy at `http://127.0.0.1:8000`
- `DEBUG=true` on the local API → email confirmation code is fixed at `123456` (saves a manual step in the test)
**Step 1: Pick a clean run by wiping prior simulator state**
Run: `xcrun simctl uninstall booted com.myhoneydue.honeyDue.dev`
Run: `docker compose -f /Users/treyt/Desktop/code/honeyDue/honeyDueAPI-go/docker-compose.dev.yml down && docker volume rm honeydueapi-go_postgres_data && docker compose -f /Users/treyt/Desktop/code/honeyDue/honeyDueAPI-go/docker-compose.dev.yml up -d`
Wait until: `curl -fsS http://127.0.0.1:8000/api/health/` returns 200.
**Step 2: Audit accessibility identifiers along the test path**
The test taps and asserts on these SwiftUI surfaces. Open `iosApp/iosApp/Helpers/AccessibilityIdentifiers.swift` and verify (or add) identifiers for each. Use stable, non-localized strings.
Surfaces to identify:
| Where | Why the test needs it | Suggested ID constant |
|---|---|---|
| Login/Register screen — username, email, password, first/last name fields, "Register" button, "Verify" code field | Drive registration | already in `AccessibilityIdentifiers.Authentication.*` per existing UI tests — verify by grep |
| Onboarding — residence-creation form (name field + Continue) | Drive residence creation | `AccessibilityIdentifiers.Onboarding.residenceNameField`, `.continueButton` (add if missing) |
| Onboarding First-Task screen — "Browse All" tab button | Switch from suggestions to browse | `AccessibilityIdentifiers.Onboarding.browseAllTab` (add if missing) |
| Onboarding First-Task screen — each template row (selectable) | Pick 3 tasks | `AccessibilityIdentifiers.Onboarding.templateRowPrefix` (e.g., `"onboarding.template.<id>"`) — see how `OnboardingFirstTaskView.swift` renders rows; add an `.accessibilityIdentifier(...)` keyed on `template.id` |
| Onboarding First-Task screen — Submit button | Trigger bulk-create | `AccessibilityIdentifiers.Onboarding.submitTasksButton` (add if missing) |
| Residence list / home — the residence cell | Tap into detail | `AccessibilityIdentifiers.Residence.cellPrefix` (e.g., `"residence.cell.<name>"` or `<id>`) — verify in `ResidenceListView` or wherever the post-onboarding landing screen renders cells |
| Residence detail — task row | Assert presence | `AccessibilityIdentifiers.Task.rowPrefix` (e.g., `"task.row"`) — verify the task list inside `TasksSectionContainer` in `ResidenceDetailView.swift:538` |
| Residence detail — empty state ("No tasks" copy) | Assert ABSENCE | `AccessibilityIdentifiers.Task.noTasksLabel` (add if missing) — find the empty-state copy in the residence-detail tasks section and pin an identifier on it |
For each missing ID, add it in two places:
1. The constant in `AccessibilityIdentifiers.swift`
2. `.accessibilityIdentifier(AccessibilityIdentifiers.X.Y)` on the SwiftUI view
Keep these app-side additions in **a single dedicated commit** so reviewers can see "test scaffolding only, no behavior change":
```bash
git -C /Users/treyt/Desktop/code/honeyDue/honeyDueKMP add -A
git -C /Users/treyt/Desktop/code/honeyDue/honeyDueKMP commit -m "test: add accessibility identifiers along the onboarding-to-residence-detail path
Scaffolding for the gitea#2 regression XCUITest. No user-visible
change — pure metadata for UI automation."
```
**Step 3: Write the failing UI test**
Create `iosApp/HoneyDueUITests/Suite11_TaskCacheRegressionTests.swift`:
```swift
import XCTest
/// Regression test for gitea#2.
///
/// Onboarding flow: register create residence pick 3 tasks submit.
/// After submit, the user lands on the home/residences screen. They tap
/// the new residence WITHOUT visiting the Tasks tab first (the Tasks tab
/// triggers a `getTasks()` that masks the bug by populating `_allTasks`).
///
/// Expected: residence detail shows 1 task row within 10s.
/// Pre-fix: residence detail shows empty state ("no tasks") forever
/// until the app is restarted.
final class Suite11_TaskCacheRegressionTests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = false
}
@MainActor
func test_tasksAppearOnResidenceDetail_afterOnboarding_withoutRestart() throws {
let app = XCUIApplication()
app.launchArguments += ["UI-Testing"]
app.launch()
// 1. Register a fresh user. Email confirmation code is fixed at "123456"
// in DEBUG mode (DEBUG_FIXED_CODES=true on the local docker stack).
let stamp = String(Int(Date().timeIntervalSince1970))
UITestHelpers.register(
in: app,
username: "uitest\(stamp)",
email: "uitest+\(stamp)@treymail.com",
password: "UItest\(stamp)!aZ",
confirmationCode: "123456"
)
// 2. Onboarding: create the residence.
UITestHelpers.completeResidenceCreation(in: app, name: "UI Test Property")
// 3. Switch to "Browse All" tab and pick 3 templates. The "For You"
// suggestions tab depends on a server-side recommendation that
// might be empty for a freshly created residence; Browse is
// deterministic.
let browseTab = app.buttons[AccessibilityIdentifiers.Onboarding.browseAllTab]
XCTAssertTrue(browseTab.waitForExistence(timeout: 5),
"Browse All tab must appear on First-Task screen")
browseTab.tap()
let templates = app.buttons.matching(
NSPredicate(format: "identifier BEGINSWITH %@",
AccessibilityIdentifiers.Onboarding.templateRowPrefix)
)
// Wait for the catalog to load fresh API call against local backend.
XCTAssertTrue(templates.element(boundBy: 0).waitForExistence(timeout: 10),
"Template catalog must load")
for i in 0..<3 {
templates.element(boundBy: i).tap()
}
// 4. Submit. This calls APILayer.bulkCreateTasks POST /api/tasks/bulk/
// The bug lives in the cache update path between this call returning
// and the residence detail screen rendering.
let submit = app.buttons[AccessibilityIdentifiers.Onboarding.submitTasksButton]
XCTAssertTrue(submit.waitForExistence(timeout: 3))
submit.tap()
// 5. Land on home/residences. Tap the residence we just created.
// Critical: do NOT visit the Tasks tab that would call getTasks()
// and populate _allTasks via setAllTasks, masking the bug.
let residenceCell = app.buttons[
AccessibilityIdentifiers.Residence.cellPrefix + "UI Test Property"
]
XCTAssertTrue(residenceCell.waitForExistence(timeout: 10),
"Residence cell must appear on home after onboarding submit")
residenceCell.tap()
// 6. Residence detail must show 1 task row, NOT the empty state.
// Generous timeout (10s) covers the network round-trip on slow
// local Docker startups.
let firstTaskRow = app.cells
.matching(NSPredicate(format: "identifier BEGINSWITH %@",
AccessibilityIdentifiers.Task.rowPrefix))
.firstMatch
XCTAssertTrue(
firstTaskRow.waitForExistence(timeout: 10),
"Tasks created during onboarding must appear on residence detail without restart (gitea#2)"
)
let emptyState = app.staticTexts[AccessibilityIdentifiers.Task.noTasksLabel]
XCTAssertFalse(
emptyState.exists,
"Empty 'no tasks' state must NOT show when tasks exist (gitea#2)"
)
// 7. Cleanup delete the test user via UI (or skip; clearing the
// docker volume between runs is the cheaper reset).
}
}
```
**Notes for the engineer writing this test:**
- `UITestHelpers.register(...)` and `UITestHelpers.completeResidenceCreation(...)` may not exist verbatim — read `iosApp/HoneyDueUITests/UITestHelpers.swift` for the existing helpers. If `register(...)` exists but doesn't take a `confirmationCode:` arg, either add one or inline the verification step.
- DO NOT use `sleep()` anywhere. Use `waitForExistence(timeout:)` everywhere. The skill `axiom-ui-testing` is loaded if you need patterns.
- `continueAfterFailure = false` so we stop at the exact assertion that fails — easier to triage video.
- If you can't get a residence cell identifier reliably (e.g., the home screen shows a custom layout, not standard cells), substitute `app.staticTexts["UI Test Property"]` and tap that. The point is to land on the residence detail without going through the Tasks tab.
**Step 4: Run the test — must FAIL**
Run:
```
xcodebuild -project iosApp/honeyDue.xcodeproj \
-scheme HoneyDueUITests \
-sdk iphonesimulator \
-destination 'platform=iOS Simulator,name=iPhone 17' \
-only-testing:HoneyDueUITests/Suite11_TaskCacheRegressionTests/test_tasksAppearOnResidenceDetail_afterOnboarding_withoutRestart \
test 2>&1 | tail -40
```
Expected: `Test Suite '...' failed.` with the assertion **"Tasks created during onboarding must appear on residence detail without restart (gitea#2)"**.
If it FAILS for a different reason (residence cell not found, timeout on browse tab, etc.) → that's an accessibility-identifier mismatch, not a bug repro. Fix the test/IDs and re-run. The test must fail SPECIFICALLY on the "no tasks on residence detail" assertion to be a valid bug capture.
If it PASSES → the bug isn't reproducing in this environment. Possibilities:
- App was already cached with `_allTasks` from a prior run (re-run Step 1 to fully wipe simulator + DB)
- The user navigated through the Tasks tab implicitly (check the home screen layout)
- The bug only happens on a code path you didn't replicate (re-read the iOS-side onboarding flow)
**Step 5: Commit the failing test**
```bash
git -C /Users/treyt/Desktop/code/honeyDue/honeyDueKMP add iosApp/HoneyDueUITests/Suite11_TaskCacheRegressionTests.swift
git -C /Users/treyt/Desktop/code/honeyDue/honeyDueKMP commit -m "test: failing — onboarding tasks must appear on residence detail without restart
Captures gitea#2 at the user-visible level. The kanban tab works but
the residence detail screen does not, until the app is restarted. This
test must FAIL at this commit and PASS after the cache unification work.
Re-run gates the merge in Task 12."
```
The test stays failing through Phase 1-3 commits. Don't run it on every commit — it's slow. Run it once at the end (Task 12).
---
## Phase 1 — TDD: catch the bug, then fix it
### Task 1: Failing test — `bulkCreateTasks` must populate `_allTasks`
**Files:**
- Create: `composeApp/src/commonTest/kotlin/com/tt/honeyDue/data/DataManagerTaskCacheTest.kt`
**Step 1: Write the failing test**
This test reproduces the onboarding bug at the cache layer. We can't easily mock Ktor here without infrastructure, so we test the cache mutation contract directly: after a successful bulk-create, `_allTasks` MUST contain every returned task, regardless of prior cache state.
```kotlin
package com.tt.honeyDue.data
import com.tt.honeyDue.models.TaskResponse
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import kotlin.test.BeforeTest
class DataManagerTaskCacheTest {
@BeforeTest
fun resetState() {
DataManager.clearAllData()
}
@Test
fun `updateTask seeds _allTasks when cache is empty`() {
// Given: fresh DataManager with no tasks loaded (the onboarding scenario)
assertEquals(null, DataManager.allTasks.value)
// When: a new task arrives via the same path bulkCreateTasks uses
val task = TaskResponse(
id = 1,
residenceId = 100,
title = "Replace HVAC filter",
kanbanColumn = "upcoming_tasks",
// ... fill remaining required TaskResponse fields with sensible defaults
)
DataManager.updateTask(task)
// Then: _allTasks is populated with this task in the right column
val allTasks = DataManager.allTasks.value
assertNotNull(allTasks, "updateTask must seed _allTasks even when it was null")
val upcomingColumn = allTasks.columns.firstOrNull { it.name == "upcoming_tasks" }
assertNotNull(upcomingColumn)
assertTrue(
upcomingColumn.tasks.any { it.id == 1 },
"task must land in the upcoming_tasks column"
)
}
}
```
You'll need to look at `TaskResponse` in `composeApp/src/commonMain/kotlin/com/tt/honeyDue/models/CustomTask.kt` to fill the required fields. Use defaults that match an onboarding-created task (no completion, no priority, due-soon date).
**Step 2: Run the test — must FAIL**
Run: `./gradlew :composeApp:testDebugUnitTest --tests "com.tt.honeyDue.data.DataManagerTaskCacheTest.updateTask seeds _allTasks when cache is empty"`
Expected: FAIL with `expected:<not null> but was:<null>` (or similar). This proves the test catches the bug.
**Step 3: Commit the failing test**
```bash
git -C /Users/treyt/Desktop/code/honeyDue/honeyDueKMP add composeApp/src/commonTest/kotlin/com/tt/honeyDue/data/DataManagerTaskCacheTest.kt
git -C /Users/treyt/Desktop/code/honeyDue/honeyDueKMP commit -m "test: failing — DataManager.updateTask must seed _allTasks"
```
### Task 2: Make `DataManager.updateTask` a real upsert
**Files:**
- Modify: `composeApp/src/commonMain/kotlin/com/tt/honeyDue/data/DataManager.kt:482-520`
**Step 1: Replace the conditional branch on `_allTasks` with an upsert**
Current (lines 484-498):
```kotlin
_allTasks.value?.let { current ->
val targetColumn = task.kanbanColumn ?: "upcoming_tasks"
val newColumns = current.columns.map { column -> ... }
_allTasks.value = current.copy(columns = newColumns)
}
```
Replace with:
```kotlin
val targetColumn = task.kanbanColumn ?: "upcoming_tasks"
val current = _allTasks.value ?: TaskColumnsResponse(
columns = standardKanbanColumns(), // see Step 2
daysThreshold = 30,
residenceId = null
)
val newColumns = current.columns.map { column ->
val filteredTasks = column.tasks.filter { it.id != task.id }
val updatedTasks = if (column.name == targetColumn) filteredTasks + task else filteredTasks
column.copy(tasks = updatedTasks, count = updatedTasks.size)
}
// If targetColumn doesn't exist in current.columns (e.g. fresh seed), append it
val finalColumns = if (newColumns.none { it.name == targetColumn }) {
newColumns + Column(name = targetColumn, tasks = listOf(task), count = 1, /* fill rest */)
} else newColumns
_allTasks.value = current.copy(columns = finalColumns)
```
**Step 2: Add `standardKanbanColumns()` helper**
Look at the backend response — `internal/repositories/task_repo.go` `GetKanbanDataForMultipleResidences` defines the column order. Mirror it in Kotlin:
```kotlin
private fun standardKanbanColumns(): List<Column> = listOf(
Column(name = "overdue_tasks", tasks = emptyList(), count = 0, /* defaults */),
Column(name = "due_soon_tasks", tasks = emptyList(), count = 0, /* defaults */),
Column(name = "in_progress_tasks", tasks = emptyList(), count = 0, /* defaults */),
Column(name = "upcoming_tasks", tasks = emptyList(), count = 0, /* defaults */),
Column(name = "completed_tasks", tasks = emptyList(), count = 0, /* defaults */),
// archived/cancelled are hidden from kanban — see honeyDueAPI-go/CLAUDE.md
)
```
Look at `Column` in `CustomTask.kt` for its required fields (display label, color, etc.). Fill in matching defaults.
**Step 3: Drop the second branch (`_tasksByResidence`) — it's going away in Phase 3**
Remove lines 500-515 entirely. The `_tasksByResidence` slot is still there for now (Phase 3 deletes it), but `updateTask` should not write to it any more.
**Step 4: Run the test — must PASS**
Run: `./gradlew :composeApp:testDebugUnitTest --tests "com.tt.honeyDue.data.DataManagerTaskCacheTest.updateTask seeds _allTasks when cache is empty"`
Expected: PASS
**Step 5: Run the full test suite to confirm no regressions**
Run: `./gradlew :composeApp:testDebugUnitTest`
Expected: same green count as Task 0 baseline + 1 new test.
**Step 6: Commit**
```bash
git -C /Users/treyt/Desktop/code/honeyDue/honeyDueKMP add composeApp/src/commonMain/kotlin/com/tt/honeyDue/data/DataManager.kt
git -C /Users/treyt/Desktop/code/honeyDue/honeyDueKMP commit -m "fix: DataManager.updateTask seeds _allTasks when cache is empty
Closes the silent no-op when _allTasks is null on first launch (the
onboarding bulkCreateTasks path). The function now upserts: builds an
empty kanban shell if needed and places the task in its target column.
Adds an unknown column at the end for forward compatibility with future
column names from the server.
Refs gitea#2"
```
### Task 3: Add upsert test for `_tasksByResidence` deletion guard
**Files:**
- Modify: `composeApp/src/commonTest/kotlin/com/tt/honeyDue/data/DataManagerTaskCacheTest.kt`
**Step 1: Add a test asserting `updateTask` does NOT touch `_tasksByResidence` any more**
```kotlin
@Test
fun `updateTask no longer mutates _tasksByResidence`() {
val before = DataManager.tasksByResidence.value
DataManager.updateTask(/* sample task */)
assertEquals(before, DataManager.tasksByResidence.value,
"updateTask must not touch _tasksByResidence — it's deprecated")
}
```
**Step 2: Run — must PASS** (we already removed the branch in Task 2)
Run: `./gradlew :composeApp:testDebugUnitTest --tests "com.tt.honeyDue.data.DataManagerTaskCacheTest.updateTask no longer mutates _tasksByResidence"`
Expected: PASS
**Step 3: Commit**
```bash
git -C /Users/treyt/Desktop/code/honeyDue/honeyDueKMP add composeApp/src/commonTest/kotlin/com/tt/honeyDue/data/DataManagerTaskCacheTest.kt
git -C /Users/treyt/Desktop/code/honeyDue/honeyDueKMP commit -m "test: lock down that updateTask no longer writes _tasksByResidence"
```
---
## Phase 2 — Belt-and-suspenders: force-refresh after mutations
The Phase 1 upsert handles the fresh-cache case correctly, but it makes assumptions about kanban column placement based on the response's `kanbanColumn` field. The server is the authoritative kanban categorizer. To eliminate any drift, also force a `_allTasks` refresh after multi-task mutations.
### Task 4: Force `_allTasks` refresh after `bulkCreateTasks`
**Files:**
- Modify: `composeApp/src/commonMain/kotlin/com/tt/honeyDue/network/APILayer.kt:647-655`
**Step 1: Add post-success refresh**
Current:
```kotlin
suspend fun bulkCreateTasks(request: BulkCreateTasksRequest): ApiResult<BulkCreateTasksResponse> {
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
val result = taskApi.bulkCreateTasks(token, request)
if (result is ApiResult.Success) {
DataManager.setTotalSummary(result.data.summary)
result.data.tasks.forEach { DataManager.updateTask(it) }
}
return result
}
```
New:
```kotlin
suspend fun bulkCreateTasks(request: BulkCreateTasksRequest): ApiResult<BulkCreateTasksResponse> {
val token = getToken() ?: return ApiResult.Error("Not authenticated", 401)
val result = taskApi.bulkCreateTasks(token, request)
if (result is ApiResult.Success) {
DataManager.setTotalSummary(result.data.summary)
// Authoritative refresh — server knows the right kanban placement.
// Cheap (one round-trip) and eliminates any client-side drift between
// the per-task kanbanColumn hint and the global kanban view.
getTasks(forceRefresh = true)
}
return result
}
```
Drop the `forEach { updateTask }` — it becomes redundant with the force-refresh.
**Step 2: Run the full test suite**
Run: `./gradlew :composeApp:testDebugUnitTest`
Expected: all green (the Phase 1 upsert tests still pass because they exercise `updateTask` directly, not `bulkCreateTasks`).
**Step 3: Commit**
```bash
git -C /Users/treyt/Desktop/code/honeyDue/honeyDueKMP add composeApp/src/commonMain/kotlin/com/tt/honeyDue/network/APILayer.kt
git -C /Users/treyt/Desktop/code/honeyDue/honeyDueKMP commit -m "fix: bulkCreateTasks force-refreshes _allTasks instead of merging task-by-task
Server is the authoritative kanban categorizer. After a bulk insert,
re-fetch /api/tasks/ so the kanban view reflects exactly what the
server sees, including any column re-categorizations the client's
in-memory upsert wouldn't compute. One extra round-trip per onboarding
submission, called once per session typically.
Refs gitea#2"
```
---
## Phase 3 — Collapse the dual cache
### Task 5: Characterization test for `getTasksForResidence`
`getTasksForResidence` already implements the filter we want to use everywhere. Lock it down with a test before we make it the primary path.
**Files:**
- Modify: `composeApp/src/commonTest/kotlin/com/tt/honeyDue/data/DataManagerTaskCacheTest.kt`
**Step 1: Add the test**
```kotlin
@Test
fun `getTasksForResidence filters _allTasks by residence id`() {
DataManager.setAllTasks(/* response with tasks across residences 100 and 200 */)
val r100 = DataManager.getTasksForResidence(100)
assertNotNull(r100)
assertTrue(r100.columns.flatMap { it.tasks }.all { it.residenceId == 100 })
val r999 = DataManager.getTasksForResidence(999)
assertNotNull(r999)
assertEquals(0, r999.columns.sumOf { it.tasks.size }) // valid id, just no tasks
}
@Test
fun `getTasksForResidence returns null when _allTasks is null`() {
DataManager.clearAllData()
assertEquals(null, DataManager.getTasksForResidence(100))
}
```
**Step 2: Run — must PASS** (no implementation change yet)
Run: `./gradlew :composeApp:testDebugUnitTest --tests "com.tt.honeyDue.data.DataManagerTaskCacheTest.getTasksForResidence*"`
Expected: PASS for both.
**Step 3: Commit**
```bash
git -C /Users/treyt/Desktop/code/honeyDue/honeyDueKMP add composeApp/src/commonTest/kotlin/com/tt/honeyDue/data/DataManagerTaskCacheTest.kt
git -C /Users/treyt/Desktop/code/honeyDue/honeyDueKMP commit -m "test: characterize getTasksForResidence filter contract"
```
### Task 6: Simplify `APILayer.getTasksByResidence`
**Files:**
- Modify: `composeApp/src/commonMain/kotlin/com/tt/honeyDue/network/APILayer.kt:591-621`
**Step 1: Replace the 3-path implementation with "ensure _allTasks fresh, then filter"**
```kotlin
suspend fun getTasksByResidence(residenceId: Int, forceRefresh: Boolean = false): ApiResult<TaskColumnsResponse> {
// Ensure _allTasks is loaded and reasonably fresh.
// getTasks itself respects forceRefresh and the global tasksCacheTime.
val allTasksResult = getTasks(forceRefresh = forceRefresh)
if (allTasksResult is ApiResult.Error) return allTasksResult
val filtered = DataManager.getTasksForResidence(residenceId)
?: return ApiResult.Error("Tasks unavailable", 0)
return ApiResult.Success(filtered)
}
```
This deletes the per-residence cache reliance entirely. `_tasksByResidence` is no longer written by this path.
**Step 2: Run the test suite**
Run: `./gradlew :composeApp:testDebugUnitTest`
Expected: all green.
**Step 3: Commit**
```bash
git -C /Users/treyt/Desktop/code/honeyDue/honeyDueKMP add composeApp/src/commonMain/kotlin/com/tt/honeyDue/network/APILayer.kt
git -C /Users/treyt/Desktop/code/honeyDue/honeyDueKMP commit -m "refactor: getTasksByResidence is now a thin filter over _allTasks
Was 3 fallback paths (per-residence cache → filter from allTasks →
network). Now: ensure _allTasks fresh, return filter. The per-residence
cache becomes write-only by this path, scheduled for deletion in the
next commit."
```
### Task 7: iOS — `TaskViewModel` observes `$allTasks` with filter
**Files:**
- Modify: `iosApp/iosApp/Task/TaskViewModel.swift:46-74`
**Step 1: Replace dual-sink with single-sink + filter**
Current logic uses two Combine sinks: `$allTasks` (only when `currentResidenceId == nil`) and `$tasksByResidence` (only when set).
Replace with one sink on `$allTasks` that conditionally filters:
```swift
DataManagerObservable.shared.$allTasks
.receive(on: DispatchQueue.main)
.sink { [weak self] allTasks in
guard let self else { return }
guard !self.isAnimatingCompletion else { return }
if let allTasks {
if let resId = self.currentResidenceId {
self.tasksResponse = self.filterByResidence(allTasks, residenceId: resId)
} else {
self.tasksResponse = allTasks
}
self.isLoadingTasks = false
}
}
.store(in: &cancellables)
```
**Step 2: Add the `filterByResidence` helper**
```swift
private func filterByResidence(_ response: TaskColumnsResponse, residenceId: Int32) -> TaskColumnsResponse {
let filteredColumns = response.columns.map { column -> Column in
let filteredTasks = column.tasks.filter { Int32($0.residenceId ?? 0) == residenceId }
return column.copy(tasks: filteredTasks, count: Int32(filteredTasks.count))
}
return response.copy(columns: filteredColumns, residenceId: String(residenceId))
}
```
(Use `.doCopy(...)` SKIE syntax if `.copy` isn't directly callable from Swift — check what other Swift code does with TaskColumnsResponse copies.)
**Step 3: Drop the `$tasksByResidence` subscription block entirely**
Remove the second `.sink` on `$tasksByResidence` (currently lines 62-74).
**Step 4: Build iOS**
Run: `xcodebuild -project iosApp/honeyDue.xcodeproj -scheme iosApp -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 17' build 2>&1 | tail -20`
Expected: `** BUILD SUCCEEDED **`
**Step 5: Run iOS unit tests**
Run: `xcodebuild -project iosApp/honeyDue.xcodeproj -scheme iosApp -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 17' -only-testing:HoneyDueTests test 2>&1 | tail -20`
Expected: TEST SUCCEEDED.
**Step 6: Commit**
```bash
git -C /Users/treyt/Desktop/code/honeyDue/honeyDueKMP add iosApp/iosApp/Task/TaskViewModel.swift
git -C /Users/treyt/Desktop/code/honeyDue/honeyDueKMP commit -m "ios: TaskViewModel observes \$allTasks and filters by residence in-memory
Single source of truth eliminates the race window where the residence
detail screen could mount before the per-residence cache slot existed.
After this, every emit of _allTasks rerenders every observing view —
kanban tab, residence detail, dashboards — atomically.
Refs gitea#2"
```
### Task 8: Android — `ResidenceViewModel` feeds `residenceTasksState` from a combined flow
**Files:**
- Modify: `composeApp/src/commonMain/kotlin/com/tt/honeyDue/viewmodel/ResidenceViewModel.kt:31-94`
**Step 1: Replace the imperative `loadResidenceTasks` with a derived flow**
Look at how `_residenceTasksState` is currently populated (line 88-94). Instead of imperatively calling `APILayer.getTasksByResidence` and storing the result, derive it:
```kotlin
private val _currentResidenceId = MutableStateFlow<Int?>(null)
val residenceTasksState: StateFlow<ApiResult<TaskColumnsResponse>> =
combine(DataManager.allTasks, _currentResidenceId) { all, id ->
when {
id == null -> ApiResult.Idle
all == null -> ApiResult.Loading
else -> {
val filtered = DataManager.getTasksForResidence(id)
if (filtered != null) ApiResult.Success(filtered) else ApiResult.Loading
}
}
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), ApiResult.Idle)
fun loadResidenceTasks(residenceId: Int, forceRefresh: Boolean = false) {
viewModelScope.launch {
_currentResidenceId.value = residenceId
// Trigger the underlying _allTasks refresh; the combine above
// re-emits Success when allTasks arrives.
APILayer.getTasks(forceRefresh = forceRefresh)
}
}
```
The screen contract (`residenceViewModel.residenceTasksState`) is preserved — `ResidenceDetailScreen.kt:59` doesn't need any change.
**Step 2: Build Android debug**
Run: `./gradlew :composeApp:assembleDebug`
Expected: BUILD SUCCESSFUL.
**Step 3: Run commonTest again**
Run: `./gradlew :composeApp:testDebugUnitTest`
Expected: all green. (`ResidenceViewModelTest` may need adjusting — check it.)
**Step 4: Commit**
```bash
git -C /Users/treyt/Desktop/code/honeyDue/honeyDueKMP add composeApp/src/commonMain/kotlin/com/tt/honeyDue/viewmodel/ResidenceViewModel.kt
# Also stage any test fixes
git -C /Users/treyt/Desktop/code/honeyDue/honeyDueKMP commit -m "android: ResidenceViewModel.residenceTasksState derives from _allTasks
Same screen contract, but the data flows from DataManager.allTasks
through a combine(...) into the existing StateFlow. No per-residence
network call needed; the upstream getTasks() refresh propagates."
```
### Task 9: Delete dead code — `_tasksByResidence` and friends
**Files:**
- Modify: `composeApp/src/commonMain/kotlin/com/tt/honeyDue/data/DataManager.kt`
- Modify: `iosApp/iosApp/Data/DataManagerObservable.swift`
**Step 1: In DataManager.kt, delete:**
- `_tasksByResidence` (line 141) and `tasksByResidence` (line 142)
- `tasksByResidenceCacheTime` (line 65)
- `setTasksForResidence` (lines 448-451)
- `invalidateTasksFor` (line 417 — verify it has no other callers first via grep)
- `_tasksByResidence` mutations in `removeTask` (lines 533-onwards) — keep only the `_allTasks` removal
- `_tasksByResidence.value = emptyMap()` in clearAllData and similar wipes (lines 783, 836)
- `tasksByResidenceCacheTime.clear()` in same wipes (lines 814, 849)
Keep `getTasksForResidence` — it's the public filter API, still used by the new `getTasksByResidence` and Android VM.
**Step 2: In DataManagerObservable.swift, delete:**
- `@Published var tasksByResidence: [Int32: TaskColumnsResponse] = [:]` (line 44)
- The `for await tasks in DataManager.shared.tasksByResidence` task (lines 195-201)
- The `tasksByResidence[residenceId]` reader at line 524 (replace with `DataManager.shared.getTasksForResidence(residenceId)` or its iOS-friendly equivalent if anything still calls this — grep first)
**Step 3: Compile both targets**
Run: `./gradlew :composeApp:assembleDebug && ./gradlew :composeApp:testDebugUnitTest`
Then: `xcodebuild -project iosApp/honeyDue.xcodeproj -scheme iosApp -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 17' build 2>&1 | tail -20`
Expected: both BUILD SUCCEEDED, all tests green.
If anything fails to compile, follow the compiler — there's likely a missed reader. Common suspects: `TaskViewModel.kt` (Kotlin VM, not Swift) line ~38-42 references `_tasksByResidenceState`; verify it's still wired correctly or also delete it.
**Step 4: Commit**
```bash
git -C /Users/treyt/Desktop/code/honeyDue/honeyDueKMP add -A
git -C /Users/treyt/Desktop/code/honeyDue/honeyDueKMP commit -m "refactor: delete _tasksByResidence and per-residence task cache plumbing
All readers and writers gone after the previous commits. Single source
of truth = DataManager._allTasks, residence views derive via
getTasksForResidence(id). Net deletion ~100 LOC across DataManager,
APILayer, DataManagerObservable, and iOS TaskViewModel.
Closes gitea#2"
```
---
## Phase 4 — Verification
### Task 10: Verify disk persistence is forward-compatible
**Files:** none (verification only)
**Step 1: Find the persistence model**
Run: `grep -rn "ignoreUnknownKeys\|Json {\|tasksByResidence" composeApp/src/commonMain/kotlin/com/tt/honeyDue/data/PersistenceManager.kt`
Expected: kotlinx.serialization Json config with `ignoreUnknownKeys = true`. If NOT, an existing user upgrading the app will crash on first launch when the persisted blob has the now-removed `tasksByResidence` field.
**Step 2: If `ignoreUnknownKeys` is missing, ADD IT BEFORE SHIPPING**
Edit the Json config:
```kotlin
val json = Json {
ignoreUnknownKeys = true
// ... existing config
}
```
Commit separately as `chore: persistence Json must ignoreUnknownKeys`.
**Step 3: Manual test — wipe simulator app data, install old app, then this version**
If you have a TestFlight build of the previous version:
1. Install old version → register → create residence → quit
2. Update to this build → launch → confirm no crash, data loads
3. Quit → relaunch → confirm persistence works correctly
If no old TestFlight build available, skip this empirical check but the `ignoreUnknownKeys` setting is sufficient.
### Task 11: Manual smoke — the actual bug repro
**Files:** none (manual test)
**Step 1: Wipe simulator state for the dev build**
Run: `xcrun simctl uninstall booted com.myhoneydue.honeyDue.dev`
**Step 2: Confirm iOS is on LOCAL (set during pre-flight, stays uncommitted)**
Run: `grep "CURRENT_ENV" /Users/treyt/Desktop/code/honeyDue/honeyDueKMP/composeApp/src/commonMain/kotlin/com/tt/honeyDue/network/ApiConfig.kt`
Expected: `val CURRENT_ENV = Environment.LOCAL`. If it's not, edit the file. DO NOT COMMIT — revert in Step 5.
**Step 3: Build and install**
Run: `./gradlew :composeApp:assembleDebug` and Xcode build to simulator.
**Step 4: Reproduce the original bug path**
1. Launch app → land on register screen
2. Register a fresh user with a unique email
3. Onboarding: create residence → choose 3+ tasks from the catalog → submit
4. Land on home/dashboard
5. Navigate to the new residence's detail screen WITHOUT visiting the Tasks tab first
6. **Expected: tasks visible immediately. No "no tasks" state. No restart needed.**
If the bug still reproduces, return to Phase 1 — the upsert or refresh isn't working. Capture iOS console with `xclog launch com.myhoneydue.honeyDue.dev` and inspect.
**Step 5: Revert the ApiConfig change**
Edit `ApiConfig.kt` back to `Environment.PROD` (or whatever it was). Confirm with `git diff`. Do not commit.
### Task 12: Final regression sweep
**Files:** none (verification only)
**Step 1: Full Kotlin test suite green**
Run: `./gradlew :composeApp:testDebugUnitTest`
**Step 2: iOS unit tests green**
Run: `xcodebuild -project iosApp/honeyDue.xcodeproj -scheme HoneyDue -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 17' -only-testing:HoneyDueTests test 2>&1 | tail -20`
**Step 3: The Task 0.5 regression XCUITest now passes**
Wipe state for a clean run (mirrors Task 0.5 Step 1):
```
xcrun simctl uninstall booted com.myhoneydue.honeyDue.dev
docker compose -f /Users/treyt/Desktop/code/honeyDue/honeyDueAPI-go/docker-compose.dev.yml down
docker volume rm honeydueapi-go_postgres_data
docker compose -f /Users/treyt/Desktop/code/honeyDue/honeyDueAPI-go/docker-compose.dev.yml up -d
```
Wait for `curl -fsS http://127.0.0.1:8000/api/health/` → 200. Then re-run the regression test:
```
xcodebuild -project iosApp/honeyDue.xcodeproj \
-scheme HoneyDueUITests \
-sdk iphonesimulator \
-destination 'platform=iOS Simulator,name=iPhone 17' \
-only-testing:HoneyDueUITests/Suite11_TaskCacheRegressionTests/test_tasksAppearOnResidenceDetail_afterOnboarding_withoutRestart \
test 2>&1 | tail -20
```
Expected: **TEST SUCCEEDED**. The "Tasks created during onboarding must appear on residence detail without restart" assertion now holds.
**If it FAILS:** the cache fix is incomplete. Inspect the test report video (`xcrun xcresulttool get --path build/reports/...xcresult ...`) and follow the failure point. Common causes: missed updateTask call site in the dual-cache deletion, a residual reader of `_tasksByResidence` in iOS not pruned, or a race between `getTasks(forceRefresh=true)` and the residence detail's first observation. **DO NOT** weaken the test to make it pass — fix the underlying issue.
**Step 4: Stress run (catch flakiness before merge)**
Run the test 5× to confirm it's stable, not just lucky:
```
for i in 1 2 3 4 5; do
xcrun simctl uninstall booted com.myhoneydue.honeyDue.dev
docker compose -f /Users/treyt/Desktop/code/honeyDue/honeyDueAPI-go/docker-compose.dev.yml down >/dev/null
docker volume rm honeydueapi-go_postgres_data >/dev/null
docker compose -f /Users/treyt/Desktop/code/honeyDue/honeyDueAPI-go/docker-compose.dev.yml up -d >/dev/null
until curl -fsS http://127.0.0.1:8000/api/health/ >/dev/null 2>&1; do sleep 2; done
xcodebuild -project iosApp/honeyDue.xcodeproj -scheme HoneyDueUITests -sdk iphonesimulator \
-destination 'platform=iOS Simulator,name=iPhone 17' \
-only-testing:HoneyDueUITests/Suite11_TaskCacheRegressionTests/test_tasksAppearOnResidenceDetail_afterOnboarding_withoutRestart \
test 2>&1 | tail -3
echo "=== run $i done ==="
done
```
Expected: 5/5 TEST SUCCEEDED. If even one fails, treat as flaky — don't merge.
**Step 5: Diff summary**
Run: `git -C /Users/treyt/Desktop/code/honeyDue/honeyDueKMP diff --stat master...HEAD`
Expected: net deletion ~80-150 lines across the listed files. If the diff is much larger, scope creep — review commits.
**Step 6: Push and open a PR (only if user confirms)**
Don't push without asking the user. Wait for explicit go-ahead.
---
## Rollback plan
If anything goes sideways in production:
1. `git revert <merge-commit-sha>` — every commit in this plan is independently revertable in reverse order, but the cleanest rollback is reverting the merge commit.
2. Old persistence blob format is preserved by `ignoreUnknownKeys` — no migration required.
3. Backend `/api/tasks/by-residence/:id/` was never touched, so a rolled-back client immediately starts using it again with no server change.
---
## Notes for the executing engineer
- **Frequent commits.** Every task ends with a commit. If you deviate from the plan, commit before deviating.
- **Don't auto-commit any other changes.** Per `honeyDueKMP/CLAUDE.md`: "DO NOT auto-commit code changes." Commit only what's specified.
- **Don't push to remote.** Let the user trigger the push after they review.
- **TaskColumnsResponse fields.** The `Column` data class in `CustomTask.kt` may have more fields than shown (display label, color, sort order). Read it before writing the standard column shell in Task 2 — the test will fail on missing required constructor args.
- **TaskResponse fields.** Same — has many fields. For test fixtures, build a small helper:
```kotlin
private fun sampleTask(id: Int, residenceId: Int, column: String) = TaskResponse(...)
```
in the test file rather than repeating the giant constructor.
- **SKIE/Swift copy.** TaskColumnsResponse `.copy()` from Swift may need `.doCopy(...)` if SKIE renamed it. Check `iosApp/iosApp/Task/TaskViewModel.swift` line 387 onward for an existing example of how Swift copies a Kotlin data class.
- **Don't refactor "while you're here."** This plan is laser-focused on the cache unification. Other smells you spot — log them, don't fix them in this PR.
+1 -1
View File
@@ -15,6 +15,6 @@
<key>manageAppVersionAndBuildNumber</key> <key>manageAppVersionAndBuildNumber</key>
<true/> <true/>
<key>teamID</key> <key>teamID</key>
<string>V3PF3M6B6U</string> <string>X86BR9WTLD</string>
</dict> </dict>
</plist> </plist>
+1 -1
View File
@@ -148,7 +148,7 @@ final class WidgetActionManager {
static let shared = WidgetActionManager() static let shared = WidgetActionManager()
private let appGroupIdentifier: String = { private let appGroupIdentifier: String = {
Bundle.main.infoDictionary?["AppGroupIdentifier"] as? String ?? "group.com.tt.honeyDue.dev" Bundle.main.infoDictionary?["AppGroupIdentifier"] as? String ?? "group.com.myhoneydue.honeyDue.dev"
}() }()
private let pendingTasksFileName = "widget_pending_tasks.json" private let pendingTasksFileName = "widget_pending_tasks.json"
private let tokenKey = "widget_auth_token" private let tokenKey = "widget_auth_token"
+1 -1
View File
@@ -111,7 +111,7 @@ class CacheManager {
} }
private static let appGroupIdentifier: String = { private static let appGroupIdentifier: String = {
Bundle.main.infoDictionary?["AppGroupIdentifier"] as? String ?? "group.com.tt.honeyDue.dev" Bundle.main.infoDictionary?["AppGroupIdentifier"] as? String ?? "group.com.myhoneydue.honeyDue.dev"
}() }()
private static let tasksFileName = "widget_tasks.json" private static let tasksFileName = "widget_tasks.json"
+2 -2
View File
@@ -9,13 +9,13 @@
} }
], ],
"defaultOptions" : { "defaultOptions" : {
"testTimeoutsEnabled" : true,
"defaultTestExecutionTimeAllowance" : 300, "defaultTestExecutionTimeAllowance" : 300,
"targetForVariableExpansion" : { "targetForVariableExpansion" : {
"containerPath" : "container:honeyDue.xcodeproj", "containerPath" : "container:honeyDue.xcodeproj",
"identifier" : "D4ADB376A7A4CFB73469E173", "identifier" : "D4ADB376A7A4CFB73469E173",
"name" : "HoneyDue" "name" : "HoneyDue"
} },
"testTimeoutsEnabled" : true
}, },
"testTargets" : [ "testTargets" : [
{ {
+34 -32
View File
@@ -663,7 +663,7 @@
0248CABA5A5197845F2E5C26 /* Release */ = { 0248CABA5A5197845F2E5C26 /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
APP_GROUP_IDENTIFIER = group.com.tt.honeyDue; APP_GROUP_IDENTIFIER = group.com.myhoneydue.honeyDue;
ARCHS = arm64; ARCHS = arm64;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
@@ -671,14 +671,15 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
DEVELOPMENT_TEAM = V3PF3M6B6U; DEVELOPMENT_TEAM = X86BR9WTLD;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = iosApp/Info.plist; INFOPLIST_FILE = iosApp/Info.plist;
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = NO; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = NO;
INFOPLIST_KEY_NSCameraUsageDescription = "honeyDue needs access to your camera to take photos of completed tasks"; INFOPLIST_KEY_NSCameraUsageDescription = "honeyDue needs camera access to take photos of tasks, documents, and receipts.";
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "honeyDue needs access to your photo library to select photos of completed tasks"; INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "honeyDue needs permission to save photos to your library.";
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "honeyDue needs photo library access to attach photos to tasks and documents.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -688,7 +689,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.tt.honeyDue; PRODUCT_BUNDLE_IDENTIFIER = com.myhoneydue.honeyDue;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
@@ -698,14 +699,14 @@
1C0789552EBC218D00392B46 /* Debug */ = { 1C0789552EBC218D00392B46 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
APP_GROUP_IDENTIFIER = group.com.tt.honeyDue.dev; APP_GROUP_IDENTIFIER = group.com.myhoneydue.honeyDue.dev;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = HoneyDueExtension.entitlements; CODE_SIGN_ENTITLEMENTS = HoneyDueExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = V3PF3M6B6U; DEVELOPMENT_TEAM = X86BR9WTLD;
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = HoneyDue/Info.plist; INFOPLIST_FILE = HoneyDue/Info.plist;
@@ -719,7 +720,7 @@
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
OTHER_SWIFT_FLAGS = "-DWIDGET_EXTENSION"; OTHER_SWIFT_FLAGS = "-DWIDGET_EXTENSION";
PRODUCT_BUNDLE_IDENTIFIER = com.tt.honeyDue.dev.HoneyDueExtension; PRODUCT_BUNDLE_IDENTIFIER = com.myhoneydue.honeyDue.dev.HoneyDueExtension;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -734,14 +735,14 @@
1C0789562EBC218D00392B46 /* Release */ = { 1C0789562EBC218D00392B46 /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
APP_GROUP_IDENTIFIER = group.com.tt.honeyDue; APP_GROUP_IDENTIFIER = group.com.myhoneydue.honeyDue;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = HoneyDueExtension.entitlements; CODE_SIGN_ENTITLEMENTS = HoneyDueExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = V3PF3M6B6U; DEVELOPMENT_TEAM = X86BR9WTLD;
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = HoneyDue/Info.plist; INFOPLIST_FILE = HoneyDue/Info.plist;
@@ -755,7 +756,7 @@
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
OTHER_SWIFT_FLAGS = "-DWIDGET_EXTENSION"; OTHER_SWIFT_FLAGS = "-DWIDGET_EXTENSION";
PRODUCT_BUNDLE_IDENTIFIER = com.tt.honeyDue.HoneyDueExtension; PRODUCT_BUNDLE_IDENTIFIER = com.myhoneydue.honeyDue.HoneyDueExtension;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -774,12 +775,12 @@
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = V3PF3M6B6U; DEVELOPMENT_TEAM = X86BR9WTLD;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.1; IPHONEOS_DEPLOYMENT_TARGET = 26.1;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.tt.HoneyDueTests; PRODUCT_BUNDLE_IDENTIFIER = com.myhoneydue.HoneyDueTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO; STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES;
@@ -800,12 +801,12 @@
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = V3PF3M6B6U; DEVELOPMENT_TEAM = X86BR9WTLD;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.1; IPHONEOS_DEPLOYMENT_TARGET = 26.1;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.tt.HoneyDueTests; PRODUCT_BUNDLE_IDENTIFIER = com.myhoneydue.HoneyDueTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO; STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES;
@@ -825,7 +826,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = V3PF3M6B6U; DEVELOPMENT_TEAM = X86BR9WTLD;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = HoneyDueQLPreview/Info.plist; INFOPLIST_FILE = HoneyDueQLPreview/Info.plist;
@@ -838,7 +839,7 @@
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.tt.honeyDue.dev.HoneyDueQLPreview; PRODUCT_BUNDLE_IDENTIFIER = com.myhoneydue.honeyDue.dev.HoneyDueQLPreview;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -856,7 +857,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = V3PF3M6B6U; DEVELOPMENT_TEAM = X86BR9WTLD;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = HoneyDueQLPreview/Info.plist; INFOPLIST_FILE = HoneyDueQLPreview/Info.plist;
@@ -869,7 +870,7 @@
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.tt.honeyDue.HoneyDueQLPreview; PRODUCT_BUNDLE_IDENTIFIER = com.myhoneydue.honeyDue.HoneyDueQLPreview;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -887,7 +888,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = V3PF3M6B6U; DEVELOPMENT_TEAM = X86BR9WTLD;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = HoneyDueQLThumbnail/Info.plist; INFOPLIST_FILE = HoneyDueQLThumbnail/Info.plist;
@@ -900,7 +901,7 @@
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.tt.honeyDue.dev.HoneyDueQLThumbnail; PRODUCT_BUNDLE_IDENTIFIER = com.myhoneydue.honeyDue.dev.HoneyDueQLThumbnail;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -918,7 +919,7 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = V3PF3M6B6U; DEVELOPMENT_TEAM = X86BR9WTLD;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = HoneyDueQLThumbnail/Info.plist; INFOPLIST_FILE = HoneyDueQLThumbnail/Info.plist;
@@ -931,7 +932,7 @@
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.tt.honeyDue.HoneyDueQLThumbnail; PRODUCT_BUNDLE_IDENTIFIER = com.myhoneydue.honeyDue.HoneyDueQLThumbnail;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -948,13 +949,13 @@
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = V3PF3M6B6U; DEVELOPMENT_TEAM = X86BR9WTLD;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.1; IPHONEOS_DEPLOYMENT_TARGET = 26.1;
MACOSX_DEPLOYMENT_TARGET = 26.1; MACOSX_DEPLOYMENT_TARGET = 26.1;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.tt.HoneyDueUITests; PRODUCT_BUNDLE_IDENTIFIER = com.myhoneydue.HoneyDueUITests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto; SDKROOT = auto;
STRING_CATALOG_GENERATE_SYMBOLS = NO; STRING_CATALOG_GENERATE_SYMBOLS = NO;
@@ -974,13 +975,13 @@
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = V3PF3M6B6U; DEVELOPMENT_TEAM = X86BR9WTLD;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.1; IPHONEOS_DEPLOYMENT_TARGET = 26.1;
MACOSX_DEPLOYMENT_TARGET = 26.1; MACOSX_DEPLOYMENT_TARGET = 26.1;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.tt.HoneyDueUITests; PRODUCT_BUNDLE_IDENTIFIER = com.myhoneydue.HoneyDueUITests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto; SDKROOT = auto;
STRING_CATALOG_GENERATE_SYMBOLS = NO; STRING_CATALOG_GENERATE_SYMBOLS = NO;
@@ -1123,7 +1124,7 @@
E767E942685C7832D51FF978 /* Debug */ = { E767E942685C7832D51FF978 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
APP_GROUP_IDENTIFIER = group.com.tt.honeyDue.dev; APP_GROUP_IDENTIFIER = group.com.myhoneydue.honeyDue.dev;
ARCHS = arm64; ARCHS = arm64;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
@@ -1131,14 +1132,15 @@
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\"";
DEVELOPMENT_TEAM = V3PF3M6B6U; DEVELOPMENT_TEAM = X86BR9WTLD;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = iosApp/Info.plist; INFOPLIST_FILE = iosApp/Info.plist;
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = NO; INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = NO;
INFOPLIST_KEY_NSCameraUsageDescription = "honeyDue needs access to your camera to take photos of completed tasks"; INFOPLIST_KEY_NSCameraUsageDescription = "honeyDue needs camera access to take photos of tasks, documents, and receipts.";
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "honeyDue needs access to your photo library to select photos of completed tasks"; INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "honeyDue needs permission to save photos to your library.";
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "honeyDue needs photo library access to attach photos to tasks and documents.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -1148,7 +1150,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.tt.honeyDue.dev; PRODUCT_BUNDLE_IDENTIFIER = com.myhoneydue.honeyDue.dev;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
@@ -16,7 +16,7 @@ final class BackgroundTaskManager {
static let shared = BackgroundTaskManager() static let shared = BackgroundTaskManager()
/// Background task identifier - must match Info.plist BGTaskSchedulerPermittedIdentifiers /// Background task identifier - must match Info.plist BGTaskSchedulerPermittedIdentifiers
static let taskIdentifier = "com.tt.honeyDue.refresh" static let taskIdentifier = "com.myhoneydue.honeyDue.refresh"
/// Time window for overnight refresh (12:00 AM - 4:00 AM) /// Time window for overnight refresh (12:00 AM - 4:00 AM)
private let refreshWindowStartHour = 0 // 12:00 AM private let refreshWindowStartHour = 0 // 12:00 AM
@@ -187,7 +187,7 @@ final class BackgroundTaskManager {
/// Force a background refresh for testing (only works in debug builds with Xcode) /// Force a background refresh for testing (only works in debug builds with Xcode)
/// Usage: In Xcode debugger console: /// Usage: In Xcode debugger console:
/// e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.tt.honeyDue.refresh"] /// e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.myhoneydue.honeyDue.refresh"]
func debugInfo() -> String { func debugInfo() -> String {
return """ return """
Background Task Debug Info: Background Task Debug Info:
@@ -48,6 +48,9 @@ struct AccessibilityIdentifiers {
static let addButton = "Residence.AddButton" static let addButton = "Residence.AddButton"
static let residencesList = "Residence.List" static let residencesList = "Residence.List"
static let residenceCard = "Residence.Card" static let residenceCard = "Residence.Card"
/// Prefix for individual residence cells in the list. Suffix with the
/// residence id to address a specific cell (e.g. "Residence.Cell.42").
static let cellPrefix = "Residence.Cell"
static let emptyStateView = "Residence.EmptyState" static let emptyStateView = "Residence.EmptyState"
static let emptyStateButton = "Residence.EmptyState.AddButton" static let emptyStateButton = "Residence.EmptyState.AddButton"
@@ -87,7 +90,15 @@ struct AccessibilityIdentifiers {
static let refreshButton = "Task.RefreshButton" static let refreshButton = "Task.RefreshButton"
static let tasksList = "Task.List" static let tasksList = "Task.List"
static let taskCard = "Task.Card" static let taskCard = "Task.Card"
/// Prefix for individual task rows. Suffix with the task id to
/// address a specific row (e.g. "Task.Row.42"). Use `BEGINSWITH`
/// in tests to detect "any task row exists".
static let rowPrefix = "Task.Row"
static let emptyStateView = "Task.EmptyState" static let emptyStateView = "Task.EmptyState"
/// Label rendered when a residence-detail tasks section has no tasks
/// in any kanban column. Asserted ABSENT after onboarding bulk-create
/// in the gitea#2 regression test.
static let noTasksLabel = "Task.NoTasksLabel"
static let kanbanView = "Task.KanbanView" static let kanbanView = "Task.KanbanView"
static let overdueColumn = "Task.Column.Overdue" static let overdueColumn = "Task.Column.Overdue"
static let upcomingColumn = "Task.Column.Upcoming" static let upcomingColumn = "Task.Column.Upcoming"
@@ -229,8 +240,24 @@ struct AccessibilityIdentifiers {
static let taskSelectionCounter = "Onboarding.TaskSelectionCounter" static let taskSelectionCounter = "Onboarding.TaskSelectionCounter"
static let addPopularTasksButton = "Onboarding.AddPopularTasksButton" static let addPopularTasksButton = "Onboarding.AddPopularTasksButton"
static let addTasksContinueButton = "Onboarding.AddTasksContinueButton" static let addTasksContinueButton = "Onboarding.AddTasksContinueButton"
/// Submit/continue button at the bottom of the First-Task screen.
/// Triggers `POST /api/tasks/bulk/` for the selected templates.
static let submitTasksButton = "Onboarding.SubmitTasksButton"
/// Tab bar control above the task list. The "Browse All" segment is
/// addressed via `app.buttons["Browse All"]` from the segmented
/// picker once this identifier is set.
static let firstTaskTabBar = "Onboarding.FirstTaskTabBar"
/// Tab segment that shows the full template catalog.
/// Tap from a test by addressing the Picker's segment label
/// "Browse All" within the element identified above.
static let browseAllTab = "Onboarding.BrowseAllTab"
static let taskCategorySection = "Onboarding.TaskCategorySection" static let taskCategorySection = "Onboarding.TaskCategorySection"
static let taskTemplateRow = "Onboarding.TaskTemplateRow" static let taskTemplateRow = "Onboarding.TaskTemplateRow"
/// Prefix for individual template rows on the First-Task screen
/// (Browse All tab). Suffix with the backend template id
/// e.g. `"Onboarding.TemplateRow.123"`. Tests use `BEGINSWITH` to
/// pick the first N rows deterministically without knowing ids.
static let templateRowPrefix = "Onboarding.TemplateRow"
// Subscription Screen // Subscription Screen
static let subscriptionTitle = "Onboarding.SubscriptionTitle" static let subscriptionTitle = "Onboarding.SubscriptionTitle"
+1 -1
View File
@@ -61,7 +61,7 @@ enum ThemeID: String, CaseIterable, Codable {
// MARK: - Shared App Group UserDefaults // MARK: - Shared App Group UserDefaults
private let appGroupID: String = { private let appGroupID: String = {
Bundle.main.infoDictionary?["AppGroupIdentifier"] as? String ?? "group.com.tt.honeyDue.dev" Bundle.main.infoDictionary?["AppGroupIdentifier"] as? String ?? "group.com.myhoneydue.honeyDue.dev"
}() }()
private let sharedDefaults: UserDefaults = { private let sharedDefaults: UserDefaults = {
guard let defaults = UserDefaults(suiteName: appGroupID) else { guard let defaults = UserDefaults(suiteName: appGroupID) else {
@@ -21,7 +21,7 @@ final class WidgetDataManager {
static let cancelledColumn = "cancelled_tasks" static let cancelledColumn = "cancelled_tasks"
private let appGroupIdentifier: String = { private let appGroupIdentifier: String = {
Bundle.main.infoDictionary?["AppGroupIdentifier"] as? String ?? "group.com.tt.honeyDue.dev" Bundle.main.infoDictionary?["AppGroupIdentifier"] as? String ?? "group.com.myhoneydue.honeyDue.dev"
}() }()
private let tasksFileName = "widget_tasks.json" private let tasksFileName = "widget_tasks.json"
private let actionsFileName = "widget_pending_actions.json" private let actionsFileName = "widget_pending_actions.json"
+7 -13
View File
@@ -6,14 +6,8 @@
<string>$(APP_GROUP_IDENTIFIER)</string> <string>$(APP_GROUP_IDENTIFIER)</string>
<key>BGTaskSchedulerPermittedIdentifiers</key> <key>BGTaskSchedulerPermittedIdentifiers</key>
<array> <array>
<string>com.tt.honeyDue.refresh</string> <string>com.myhoneydue.honeyDue.refresh</string>
</array> </array>
<key>HONEYDUE_IAP_ANNUAL_PRODUCT_ID</key>
<string>com.tt.honeyDue.pro.annual</string>
<key>HONEYDUE_IAP_MONTHLY_PRODUCT_ID</key>
<string>com.tt.honeyDue.pro.monthly</string>
<key>HONEYDUE_GOOGLE_WEB_CLIENT_ID</key>
<string></string>
<key>CFBundleDocumentTypes</key> <key>CFBundleDocumentTypes</key>
<array> <array>
<dict> <dict>
@@ -40,17 +34,17 @@
</array> </array>
</dict> </dict>
</array> </array>
<key>HONEYDUE_GOOGLE_WEB_CLIENT_ID</key>
<string></string>
<key>HONEYDUE_IAP_ANNUAL_PRODUCT_ID</key>
<string>com.myhoneydue.honeyDue.pro.annual</string>
<key>HONEYDUE_IAP_MONTHLY_PRODUCT_ID</key>
<string>com.myhoneydue.honeyDue.pro.monthly</string>
<key>NSAppTransportSecurity</key> <key>NSAppTransportSecurity</key>
<dict> <dict>
<key>NSAllowsLocalNetworking</key> <key>NSAllowsLocalNetworking</key>
<true/> <true/>
</dict> </dict>
<key>NSCameraUsageDescription</key>
<string>honeyDue needs camera access to take photos of tasks, documents, and receipts.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>honeyDue needs photo library access to attach photos to tasks and documents.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>honeyDue needs permission to save photos to your library.</string>
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>
<array> <array>
<string>remote-notification</string> <string>remote-notification</string>
+20 -23
View File
@@ -74,6 +74,18 @@
} }
} }
}, },
"%@, %@, %lld%% match" : {
"comment" : "A row that displays a suggestion with a title, frequency, and relevance percentage.",
"isCommentAutoGenerated" : true,
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "%1$@, %2$@, %3$lld%% match"
}
}
}
},
"%@, %@%@" : { "%@, %@%@" : {
"comment" : "A button that displays the name of a product and its price.", "comment" : "A button that displays the name of a product and its price.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
@@ -154,6 +166,10 @@
} }
} }
}, },
"%lld%%" : {
"comment" : "A badge that shows the relevance of a suggestion. The argument is the relevance percentage.",
"isCommentAutoGenerated" : true
},
"•" : { "•" : {
"comment" : "A separator between different pieces of information in a text.", "comment" : "A separator between different pieces of information in a text.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
@@ -221,9 +237,6 @@
}, },
"Add document" : { "Add document" : {
},
"Add Most Popular" : {
}, },
"Add new property" : { "Add new property" : {
"comment" : "A label displayed as a button in the toolbar.", "comment" : "A label displayed as a button in the toolbar.",
@@ -17684,10 +17697,6 @@
"comment" : "A button that generates a new share code.", "comment" : "A button that generates a new share code.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Generating suggestions..." : {
"comment" : "Text displayed while the app is generating personalized task suggestions.",
"isCommentAutoGenerated" : true
},
"Get notified when someone joins your property" : { "Get notified when someone joins your property" : {
}, },
@@ -17710,16 +17719,8 @@
"comment" : "A label for the back button.", "comment" : "A label for the back button.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Good match" : {
"comment" : "A label describing a task's relevance.",
"isCommentAutoGenerated" : true
},
"Google Sign-In Error" : { "Google Sign-In Error" : {
},
"Great match" : {
"comment" : "A label describing a high-relevance task.",
"isCommentAutoGenerated" : true
}, },
"Help improve honeyDue by sharing anonymous usage data" : { "Help improve honeyDue by sharing anonymous usage data" : {
@@ -17862,10 +17863,6 @@
}, },
"No personal data is collected. Analytics are fully anonymous." : { "No personal data is collected. Analytics are fully anonymous." : {
},
"No personalized suggestions yet" : {
"comment" : "A message displayed when the user has not yet been personalized.",
"isCommentAutoGenerated" : true
}, },
"No properties yet" : { "No properties yet" : {
@@ -25444,6 +25441,10 @@
"comment" : "A button label that allows users to skip the current onboarding step.", "comment" : "A button label that allows users to skip the current onboarding step.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Skip for now" : {
"comment" : "A button label that skips onboarding.",
"isCommentAutoGenerated" : true
},
"Skip for Now" : { "Skip for Now" : {
}, },
@@ -30617,10 +30618,6 @@
"comment" : "A button label that says \"Try Again\".", "comment" : "A button label that says \"Try Again\".",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Try the Browse tab to explore tasks by category,\nor add home details for better suggestions." : {
"comment" : "A description of the benefits of using the",
"isCommentAutoGenerated" : true
},
"Unarchive" : { "Unarchive" : {
"comment" : "A button that unarchives a task.", "comment" : "A button that unarchives a task.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
@@ -136,6 +136,7 @@ struct OnboardingFirstTaskContent: View {
OnboardingTaskTabBar(selectedTab: $selectedTab) OnboardingTaskTabBar(selectedTab: $selectedTab)
.padding(.horizontal, OrganicSpacing.comfortable) .padding(.horizontal, OrganicSpacing.comfortable)
.accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.firstTaskTabBar)
switch selectedTab { switch selectedTab {
case .forYou: case .forYou:
@@ -362,6 +363,7 @@ struct OnboardingFirstTaskContent: View {
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
.naturalShadow(selectedCount > 0 ? .medium : .subtle) .naturalShadow(selectedCount > 0 ? .medium : .subtle)
} }
.accessibilityIdentifier(AccessibilityIdentifiers.Onboarding.submitTasksButton)
.disabled(vm.isSubmitting) .disabled(vm.isSubmitting)
.animation(.easeInOut(duration: 0.2), value: selectedCount) .animation(.easeInOut(duration: 0.2), value: selectedCount)
} }
@@ -631,6 +633,7 @@ private struct OnboardingSuggestionRow: View {
.contentShape(Rectangle()) .contentShape(Rectangle())
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.accessibilityIdentifier("\(AccessibilityIdentifiers.Onboarding.templateRowPrefix).\(suggestion.template.id)")
.accessibilityLabel("\(suggestion.template.title), \(suggestion.template.frequencyDisplay), \(relevancePercent)% match") .accessibilityLabel("\(suggestion.template.title), \(suggestion.template.frequencyDisplay), \(relevancePercent)% match")
.accessibilityValue(isSelected ? "selected" : "not selected") .accessibilityValue(isSelected ? "selected" : "not selected")
} }
@@ -776,6 +779,7 @@ private struct OnboardingTemplateRow: View {
.contentShape(Rectangle()) .contentShape(Rectangle())
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.accessibilityIdentifier("\(AccessibilityIdentifiers.Onboarding.templateRowPrefix).\(template.id)")
.accessibilityLabel("\(template.title), \(template.frequencyLabel)") .accessibilityLabel("\(template.title), \(template.frequencyLabel)")
.accessibilityValue(isSelected ? "selected" : "not selected") .accessibilityValue(isSelected ? "selected" : "not selected")
} }
@@ -231,6 +231,7 @@ private struct ResidencesContent: View {
.padding(.horizontal, 16) .padding(.horizontal, 16)
} }
.buttonStyle(OrganicCardButtonStyle()) .buttonStyle(OrganicCardButtonStyle())
.accessibilityIdentifier("\(AccessibilityIdentifiers.Residence.cellPrefix).\(residence.id)")
.transition(.asymmetric( .transition(.asymmetric(
insertion: .opacity.combined(with: .move(edge: .bottom)), insertion: .opacity.combined(with: .move(edge: .bottom)),
removal: .opacity removal: .opacity
@@ -7,7 +7,7 @@ import ComposeApp
final class KeychainHelper: NSObject, KeychainDelegate { final class KeychainHelper: NSObject, KeychainDelegate {
static let shared = KeychainHelper() static let shared = KeychainHelper()
private let service = "com.tt.honeyDue" private let service = "com.myhoneydue.honeyDue"
func save(key: String, value: String) -> Bool { func save(key: String, value: String) -> Bool {
guard let data = value.data(using: .utf8) else { return false } guard let data = value.data(using: .utf8) else { return false }
@@ -14,8 +14,8 @@ class StoreKitManager: ObservableObject {
// Canonical source: SubscriptionProducts in commonMain (Kotlin shared code). // Canonical source: SubscriptionProducts in commonMain (Kotlin shared code).
// Keep these in sync with SubscriptionProducts.MONTHLY / SubscriptionProducts.ANNUAL. // Keep these in sync with SubscriptionProducts.MONTHLY / SubscriptionProducts.ANNUAL.
private let fallbackProductIDs = [ private let fallbackProductIDs = [
"com.tt.honeyDue.pro.monthly", "com.myhoneydue.honeyDue.pro.monthly",
"com.tt.honeyDue.pro.annual" "com.myhoneydue.honeyDue.pro.annual"
] ]
private var configuredProductIDs: [String] { private var configuredProductIDs: [String] {
@@ -122,6 +122,7 @@ struct DynamicTaskCard: View {
.cornerRadius(12) .cornerRadius(12)
.shadow(color: Color.black.opacity(0.1), radius: 5, x: 0, y: 2) .shadow(color: Color.black.opacity(0.1), radius: 5, x: 0, y: 2)
.simultaneousGesture(TapGesture(), including: .subviews) .simultaneousGesture(TapGesture(), including: .subviews)
.accessibilityIdentifier("\(AccessibilityIdentifiers.Task.rowPrefix).\(task.id)")
.sheet(isPresented: $showCompletionHistory) { .sheet(isPresented: $showCompletionHistory) {
CompletionHistorySheet( CompletionHistorySheet(
taskTitle: task.title, taskTitle: task.title,
@@ -22,6 +22,8 @@ struct EmptyTasksView: View {
.background(Color.appBackgroundSecondary) .background(Color.appBackgroundSecondary)
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
.naturalShadow(.subtle) .naturalShadow(.subtle)
.accessibilityElement(children: .combine)
.accessibilityIdentifier(AccessibilityIdentifiers.Task.noTasksLabel)
} }
} }