Adds a one-shot SnapshotTesting case that renders the new
`PreviewViewController.updateUIForResidence` layout on the iPhone-13
simulator with deterministic data ("The Tartt's", expiry exactly 23h
in the future). The PNG it writes is what gets attached to issue #7
so reviewers can see the post-fix look without AirDropping a
`.honeydue` file to a device.
`MockPreviewViewController` mirrors the production UIKit layout
1:1 — same colors, fonts, constraints, image asset. (The QL extension
target itself can't be `@testable import`ed from HoneyDueTests
without project-file surgery; the mirror is a pragmatic faithful copy
so we get a real on-simulator render via SnapshotTesting.)
The included PNG is the recorded golden.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Issue #7 called out four problems with the QuickLook preview iOS
recipients see when they open a `.honeydue` invite (e.g. via AirDrop or
Save to Files). All four fixed here.
1. Filename: keep spaces and apostrophes
`HoneyDueShareCodec.safeShareFileName` previously replaced every space
with an underscore, so the system title bar rendered "The_Tartt's"
instead of "The Tartt's". Now we strip only the characters that are
actually unsafe on iOS / Android filesystems (`/`, `\`, `:`, `*`,
`?`, `"`, `<`, `>`, `|`, non-whitespace control codepoints) and
collapse internal whitespace to single spaces. Locked in with six
new commonTest cases.
2. Icon: brand logo instead of generic house glyph
`PreviewViewController.updateUIForResidence` was using
`UIImage(systemName: "house.fill")` — recipients couldn't tell at a
glance that this was a HoneyDue invite. The honeyDue app logo
(Assets.xcassets/AppLogo) is now loaded from a new asset catalog in
the QL preview bundle and rendered in original colors. SF Symbol
fallback retained for any asset-load failure.
3. Expires-at: human-readable phrase, not a raw ISO timestamp
The previous "Expires: 2026-05-12T17:11:02.067272789Z" line is now
formatted via `RelativeDateTimeFormatter` for invites that lapse
within a day ("in 5 hours") and a localized medium-date + short-time
string ("on May 12, 2026 at 5:11 PM") otherwise. Already-expired
links render "expired 2 hours ago". Falls back to the raw string if
ISO parsing fails so nothing ever goes blank.
4. Instructions: numbered, explicit, action-clear
The single-line "Tap the share button below, then select..." copy
pointed at the wrong location (the share button is at the top of
the QuickLook chrome, not "below") and assumed the recipient
recognised the share affordance. Replaced with a three-step list.
Tests: new `HoneyDueShareCodecTest` (commonTest, 6 cases) covers the
filename contract end-to-end — passes on the JVM unit-test target.
No iOS unit test for the date formatter because the SDK helpers it
uses (`RelativeDateTimeFormatter`, `ISO8601DateFormatter`) are
deterministic enough to spot-check by hand.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backblaze B2's S3-compatible endpoint does not implement the S3 POST
Object operation — every POST returns HTTP 501 regardless of URL form
(path-style or virtual-hosted-style). The previous multipart-POST flow
has been failing for every task-completion image upload.
Server-side companion change (honeyDueAPI master @7cc5448) replaces
PresignedPostPolicy with PresignHeader/PUT and renames the response
field from "fields" to "headers". This commit aligns both clients.
PresignUploadResponse model: field renamed `fields` → `headers`,
added `method` (default "PUT"). Both new fields have defaults so a
build talking to a stale server still decodes — albeit with empty
headers, which would then 403 at signature time. The server is
already on the new shape in prod.
iOS PresignedUploader.swift: dropped the ~70-line multipart body
builder and S3 form-field ordering logic. Replaced with a single PUT
request that applies server-supplied headers verbatim (skipping
Content-Length, which URLSession sets automatically and refuses to
override).
Android UploadApi.kt: same shape change. `postToStorage` →
`putToStorage`. Single Ktor `client.put()` with headers passthrough.
`uploadOne`'s `fileName` parameter kept for source compatibility but
marked @Suppress("UNUSED_PARAMETER") since PUT doesn't need it.
Verified end-to-end against api.myhoneydue.com:
presign → PUT 12 bytes → HTTP 200 in 0.6s.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Default is `false` (current session-reuse behaviour) so tests reuse the
existing logged-in session — fast, and resilient to suites where the
current screen lacks a logout affordance (`UITestHelpers.ensureLoggedOut`
times out → tests fail before their bodies run).
Override to `true` in suites that observe transient `Invalid token` 401s
on POST/PATCH while reads continue to work. Recipe added after a 2026-05
incident where the API container was rebuilt mid-suite and in-memory
JWT tokens went stale; the diagnostic value is having an explicit lever
to reach for next time, not flipping the default.
Net effect on a clean simulator + stable API: 244/253 → 244/253 (no
behaviour change in the default path).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The XCUITest for gitea#2 (Suite11) was failing for reasons unrelated
to the cache fix — actual bugs in the registration/onboarding code
that real users probably hit too:
1. OrganicOnboardingSecureField + iOS 26 SecureField/autofill bug
On iOS 26, tapping a SwiftUI SecureField with .textContentType(.password)
doesn't reliably bring up the keyboard — the strong-password autofill
panel steals focus. Fix: under --ui-testing, default the visibility
toggle to ON so the field renders as a plain TextField (which has
reliable focus). Real users are unaffected.
2. Email registration didn't propagate auth state
Apple/Google sign-in paths called AuthenticationManager.shared.login(),
but email-registration's onChange(viewModel.isRegistered) handler did
not. As a result, AuthenticationManager.isAuthenticated stayed false
through the entire onboarding flow. OnboardingState.completeOnboarding
has an auth guard that silently no-ops when isAuthenticated is false,
leaving users stuck on the firstTask screen forever (until a
scenePhase event triggered checkAuthenticationStatus to re-sync from
DataManager). Fix: call authManager.login(verified: false) when
isRegistered flips true.
Suite11 now passes 2/2 in 96-107s, exercising the full onboarding flow
and asserting tasks appear on residence detail without restart.
Refs gitea#2
Replaces the dual-sink ($allTasks when residence-scoped is nil,
$tasksByResidence when set) with a single $allTasks observation
that filters in-memory when currentResidenceId is set.
Eliminates the gitea#2 race window where the per-residence cache slot
could be empty while $allTasks was populated, leaving residence
detail stuck on the empty state. After this commit, every emit of
_allTasks rerenders every observing view — kanban tab, residence
detail, dashboards — atomically.
Refs gitea#2
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>
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.
The KMP shared layer's task-completion-with-images path now exclusively
uses the presigned-URL flow: each image is compressed, uploaded directly
to B2 via APILayer.uploadImage, and the resulting upload_ids are passed
to /api/task-completions/ as JSON. Bytes never traverse our API server.
Changes:
- TaskCompletionViewModel.createTaskCompletionWithImages now does the
presign→POST→collect-ids dance internally. The signature stays the
same so the three Android UI call sites (TasksScreen, AllTasksScreen,
ResidenceDetailScreen, CompleteTaskDialog, CompleteTaskScreen) need
no changes.
- APILayer.createTaskCompletionWithImages removed (dead).
- TaskCompletionApi.createCompletionWithImages removed (the multipart
HTTP helper that posted to the legacy POST /api/task-completions/
multipart endpoint).
- TaskCompletionCreateRequest.imageUrls field removed.
- Three Swift call sites (CompleteTaskView, WidgetActionProcessor,
PushNotificationManager) updated to drop the imageUrls argument.
- Two Kotlin call sites (CompleteTaskDialog, CompleteTaskScreen) updated.
Image uploads now match WhatsApp/Slack-class architecture: client-side
compression + direct-to-storage upload + lightweight JSON entity create.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
iOS (Swift) — primary path, since iOS is the live platform:
- ImageDownsampler.swift: ImageIO/CGImageSourceCreateThumbnailAtIndex
based resize. Pays only the cost of the resized bitmap rather than
decoding the full source — a 12 MP iPhone photo previously
materialized ~50 MB regardless of JPEG size. Profiles: completion
(2048 px / quality 0.85), document_image (2560 px / 0.90).
- PresignedUploader.swift: three-step orchestration (POST /uploads/presign
→ multipart POST direct to B2 with the signed policy fields → return
upload_id). Maps HTTP errors to user-facing copy. Concurrent uploads
via TaskGroup.
- CompleteTaskView.swift: replaces the multipart-with-images path with
downsample → upload-to-B2 → create-completion-with-upload_ids[]. The
no-image branch unchanged.
Android (Kotlin) — parity:
- composeApp/.../media/ImageDownsampler.kt: BitmapFactory inSampleSize
+ proportional scale + JPEG compress. Same profiles as iOS.
- composeApp/.../network/UploadApi.kt: Ktor-based presign + direct-to-B2
POST. Preserves form-field order so the S3 policy signature validates.
- APILayer.uploadImage(category, contentType, bytes, fileName) → upload_id.
UI integration to follow.
Shared (Kotlin):
- models/TaskCompletion.kt: added uploadIds: List<Int>? to
TaskCompletionCreateRequest and a new PresignUploadRequest /
PresignUploadResponse pair matching the Go API DTOs.
- Existing call sites (WidgetActionProcessor, PushNotificationManager)
explicitly pass uploadIds: nil for backwards compatibility — Swift's
bridge to Kotlin doesn't honor Kotlin defaults for required-positional
parameters.
The legacy multipart path remains functional alongside the new one for
soak-test purposes; per-platform feature flags can flip between them at
any time. After zero multipart traffic in production for 7 consecutive
days, the legacy paths can be dropped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single source of truth: `com.tt.honeyDue.testing.GalleryScreens` lists
every user-reachable screen with its category (DataCarrying / DataFree)
and per-platform reachability. Both platforms' test harnesses are
CI-gated against it — `GalleryManifestParityTest` on each side fails
if the surface list drifts from the manifest.
Variant matrix by category: DataCarrying captures 4 PNGs
(empty/populated × light/dark), DataFree captures 2 (light/dark only).
Empty variants for DataCarrying use `FixtureDataManager.empty(seedLookups = false)`
so form screens that only read DM lookups can diff against populated.
Detail-screen rendering fixed on both platforms. Root cause: VM
`stateIn(Eagerly, initialValue = …)` closures evaluated
`_selectedX.value` before screen-side `LaunchedEffect` / `.onAppear`
could set the id, leaving populated captures byte-identical to empty.
Kotlin: `ContractorViewModel` + `DocumentViewModel` accept
`initialSelectedX: Int? = null` so the id is set in the primary
constructor before `stateIn` computes its seed.
Swift: `ContractorViewModel`, `DocumentViewModelWrapper`,
`ResidenceViewModel`, `OnboardingTasksViewModel` gained pre-seed
init params. `ContractorDetailView`, `DocumentDetailView`,
`ResidenceDetailView`, `OnboardingFirstTaskContent` gained
test/preview init overloads that accept the pre-seeded VM.
Corresponding view bodies prefer cached success state over
loading/error — avoids a spinner flashing over already-visible
content during background refreshes (production benefit too).
Real production bug fixed along the way: `DataManager.clear()` was
missing `_contractorDetail`, `_documentDetail`, `_contractorsByResidence`,
`_taskCompletions`, `_notificationPreferences`. On logout these maps
leaked across user sessions; in the gallery they leaked the previous
surface's populated state into the next surface's empty capture.
`ImagePicker.android.kt` guards `rememberCameraPicker` with
`LocalInspectionMode` — `FileProvider.getUriForFile` can't resolve the
Robolectric test-cache path, so `add_document` / `edit_document`
previously failed the entire capture.
Honest reclassifications: `complete_task`, `manage_users`, and
`task_suggestions` moved to DataFree. Their first-paint visible state
is driven by static props or APILayer calls, not by anything on
`IDataManager` — populated would be byte-identical to empty without
a significant production rewire. The manifest comments call this out.
Manifest counts after all moves: 43 screens = 12 DataCarrying + 31
DataFree, 37 on both platforms + 3 Android-only (home, documents,
biometric_lock) + 3 iOS-only (documents_warranties, add_task,
profile_edit).
Test results after full record:
Android: 11/11 DataCarrying diff populated vs empty
iOS: 12/12 DataCarrying diff populated vs empty
Also in this change:
- `scripts/build_parity_gallery.py` parses the Kotlin manifest
directly, renders rows in product-flow order, shows explicit
`[missing — <platform>]` placeholders for expected-but-absent
captures and muted `not on <platform>` placeholders for
platform-specific screens. Docs regenerated.
- `scripts/cleanup_orphan_goldens.sh` safely removes PNGs from prior
test configurations (theme-named, compare artifacts, legacy
empty/populated pairs for what is now DataFree). Dry-run by default.
- `docs/parity-gallery.md` rewritten: canonical-manifest workflow,
adding-a-screen guide, variant matrix explained.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the DI seam to the 5 previously singleton-coupled VMs:
- VerifyEmailViewModel
- RegisterViewModel
- PasswordResetViewModel
- AppleSignInViewModel
- OnboardingTasksViewModel
All now accept init(dataManager: DataManagerObservable = .shared).
iOSApp.swift injects DataManagerObservable.shared at the root via
.environmentObject so descendant views can reach it via @EnvironmentObject
without implicit singleton reads.
Dependencies.swift factories updated to pass DataManager.shared explicitly
into Kotlin VM constructors — SKIE doesn't surface Kotlin default init
parameters as Swift defaults, so every Kotlin VM call-site needs the
explicit argument. Affects makeAuthViewModel, makeResidenceViewModel,
makeTaskViewModel, makeContractorViewModel, makeDocumentViewModel.
Full iOS build green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- SnapshotGalleryTests rendered at displayScale: 2.0 (was native 3.0)
→ 49MB → 15MB (~69% reduction)
- Records via SNAPSHOT_TESTING_RECORD=1 env var (no code edits needed)
- scripts/optimize_goldens.sh runs zopflipng (or pngcrush fallback)
over both iOS and Android golden dirs
- scripts/{record,verify}_snapshots.sh one-command wrappers
- Makefile targets: make {record,verify,optimize}-snapshots
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Records 58 baseline PNGs across 29 primary SwiftUI screens × {light, dark}
for the honeyDue iOS app. Covers auth, password reset, onboarding,
residences, tasks, contractors, documents, profile, and subscription
surfaces — everything that's instantiable without complex runtime context.
State coverage is empty-only for this first pass: views currently spin up
their own ViewModels which read DataManagerObservable.shared directly, and
the test host has no login → all flows render their empty states. A
follow-up PR adds an optional `dataManager:` init param to each
*ViewModel.swift so populated-state snapshots (backed by P1's
FixtureDataManager) can land.
Tolerance knobs: pixelPrecision 0.97 / perceptualPrecision 0.95 — tuned to
absorb animation-frame drift (gradient blobs, focus rings) while catching
structural regressions.
Tooling: swift-snapshot-testing SPM dep added to the HoneyDueTests target
only (not the app target) via scripts/add_snapshot_testing.rb, which is an
idempotent xcodeproj-gem script so the edit is reproducible rather than a
hand-crafted pbxproj diff. Pins resolve to 1.19.2 (up-to-next-major from
the 1.17.0 plan floor).
Blocks regressions at PR time via `xcodebuild test
-only-testing:HoneyDueTests/SnapshotGalleryTests`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces DataManagerEnvironmentKey + EnvironmentValues.dataManager so
SwiftUI views can resolve DataManagerObservable via @Environment, mirroring
Compose's LocalDataManager ambient on the Kotlin side.
No view migrations yet — views continue to read DataManagerObservable.shared
directly. The actual screen-level substitution (fake DataManager for
parity-gallery / tests / previews) lands in P1 when ViewModels gain an
optional init param that accepts the environment-resolved observable. For
this commit we only need the key so P1 can wire against it.
Note: the iosSimulator Kotlin compile is broken at baseline (bb4cbd5)
with pre-existing "Unresolved reference 'testTagsAsResourceId'" errors
across 20+ screen files — Android-only semantics API imported in
commonMain. Swift-parse of the new file succeeds. Verified by checking
out bb4cbd5 and rerunning ./gradlew :composeApp:compileKotlinIosSimulatorArm64.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fix root causes uncovered across repeated parallel runs:
- Admin seed password "test1234" failed backend complexity (needs
uppercase). Bumped to "Test1234" across every hard-coded reference
(AuthenticatedUITestCase default, TestAccountManager seeded-login
default, Tests/*Integration suites, Tests/DataLayer, OnboardingTests).
- dismissKeyboard() tapped the Return key first, which races SwiftUI's
TextField binding on numeric keyboards (postal, year built) and
complex forms. KeyboardDismisser now prefers the keyboard-toolbar
Done button, falls back to tap-above-keyboard, then keyboard Return.
BaseUITestCase.clearAndEnterText uses the same helper.
- Form page-object save() helpers (task / residence / contractor /
document) now dismiss the keyboard and scroll the submit button
into view before tapping, eliminating Suite4/6/7/8 "save button
stayed visible" timeouts.
- Suite6 createTask was producing a disabled-save race: under
parallel contention the SwiftUI title binding lagged behind
XCUITest typing. Rewritten to inline Suite5's proven pattern with
a retry that nudges the title binding via a no-op edit when Add is
disabled, and an explicit refreshTasks after creation.
- Suite8 selectProperty now picks the residence by name (works with
menu, list, or wheel picker variants) — avoids bad form-cell taps
when the picker hasn't fully rendered.
- run_ui_tests.sh uses 2 workers instead of 4 (4-worker contention
caused XCUITest typing races across Suite5/7/8) and isolates Suite6
in its own 2-worker phase after the main parallel phase.
- Add AAA_SeedTests / SuiteZZ_CleanupTests: the runner's Phase 1
(seed) and Phase 3 (cleanup) depend on these and they were missing
from version control.
Both "For You" and "Browse All" tabs are now fully server-driven on
iOS and Android. No on-device task list, no client-side scoring rules.
When the API fails the screen shows error + Retry + Skip so onboarding
can still complete on a flaky network.
Shared (KMM)
- TaskCreateRequest + TaskResponse carry templateId
- New BulkCreateTasksRequest/Response, TaskApi.bulkCreateTasks,
APILayer.bulkCreateTasks (updates DataManager + TotalSummary)
- OnboardingViewModel: templatesGroupedState + loadTemplatesGrouped;
createTasks(residenceId, requests) posts once via the bulk path
- Deleted regional-template plumbing: APILayer.getRegionalTemplates,
OnboardingViewModel.loadRegionalTemplates, TaskTemplateApi.
getTemplatesByRegion, TaskTemplate.regionId/regionName
- 5 new AnalyticsEvents constants for the onboarding funnel
Android (Compose)
- OnboardingFirstTaskContent rewritten against the server catalog;
~70 lines of hardcoded taskCategories gone. Loading / Error / Empty
panes with Retry + Skip buttons. Category icons derived from name
keywords, colours from a 5-value palette keyed by category id
- Browse selection carries template.id into the bulk request so
task_template_id is populated server-side
iOS (SwiftUI)
- New OnboardingTasksViewModel (@MainActor ObservableObject) wrapping
APILayer.shared for suggestions / grouped / bulk-submit with
loading + error state (mirrors the TaskViewModel.swift pattern)
- OnboardingFirstTaskView rewritten: buildForYouSuggestions (130 lines)
and fallbackCategories (68 lines) deleted; both tabs show the same
error+skip UX as Android; ForYouSuggestion/SuggestionRelevance gone
- 5 new AnalyticsEvent cases with identical PostHog event names to
the Kotlin constants so cross-platform funnels join cleanly
- Existing TaskCreateRequest / TaskResponse call sites in TaskCard,
TasksSection, TaskFormView updated for the new templateId parameter
Docs
- CLAUDE.md gains an "Onboarding task suggestions (server-driven)"
subsection covering the data flow, key files on both platforms,
and the KotlinInt(int: template.id) wrapping requirement
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Screens.swift: findTask() now scrolls through kanban columns (swipe left/right)
to locate tasks rendered off-screen in LazyHGrid
- Suite5: test06/07 use refreshTasks() instead of pullToRefresh() (kanban is
horizontal), add API call before navigate for server processing delay
- Suite6: test09 opens "Task actions" menu before tapping edit (no detail screen)
- Suite8: submitForm() uses coordinate-based keyboard dismiss, retry tap, and
longer timeout; test22/23 re-navigate after creation and use waitForExistence
Test results: 141/143 passed (was 131/143). Remaining 2 failures are pre-existing
(Suite1 test11) and flaky/unrelated (Suite3 testR307).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix registration flow dismiss cascade: chain fullScreenCover → sheet onDismiss
so auth state is set only after all UIKit presentations are removed, preventing
RootView from swapping LoginView→MainTabView behind a stale sheet
- Fix onboarding reset: set hasCompletedOnboarding directly instead of calling
completeOnboarding() which has an auth guard that fails after DataManager.clear()
- Stabilize Suite1 registration tests, Suite6 task tests, Suite7 contractor tests
- Add clean-slate-per-suite via AuthenticatedUITestCase reset state
- Improve test account seeding and screen object reliability
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Expand Localizable.xcstrings with 426 new localization entries
- Add xctestplan files (CI, Cleanup, Parallel, Seed) for structured test runs
- Add run_ui_tests.sh script for UI test execution
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Picker: moved .fixedSize(), .padding, .background, .clipShape outside the Menu
label so the capsule sizing is stable and never clips rounded corners during
menu open/close animation.
Tab bar: replaced custom HStack+underline tab bar with native SwiftUI
Picker(.segmented) for "For You" / "Browse All" tabs.
ZIP code was US-only and redundant now that the suggestion engine
uses home profile features (heating, pool, etc.) for personalization.
Onboarding flow: Welcome → Value Props → Name → Account → Verify →
Home Profile → Task Selection (was: ...Verify → ZIP → Home Profile...)
Removed regionalTemplates references from task selection view.
Both iOS and Compose flows updated.
New onboarding step: "Tell us about your home" with chip-based pickers
for systems (heating/cooling/water heater), features (pool, fireplace,
garage, etc.), exterior (roof, siding), interior (flooring, landscaping).
All optional, skippable.
Tabbed task selection: "For You" tab shows personalized suggestions
based on home profile, "Browse All" has existing category browser.
Removed 5-task limit — users can add unlimited tasks.
Removed subscription upsell from onboarding flow — app is free.
Fixed picker capsule squishing bug with .fixedSize() modifier.
Both iOS and Compose implementations updated.
Android: uncaught exception handler sends $exception events with stack
trace to PostHog, flushes before delegating to default handler.
iOS: NSSetUncaughtExceptionHandler captures crashes via PostHogSDK,
avoids @MainActor deadlock by calling SDK directly.
Common: captureException() available for non-fatal catches app-wide.
Platform stubs for jvm/js/wasmJs.
Biometric lock: opt-in Face ID/Touch ID/fingerprint app lock with toggle
in ProfileScreen. Locks on background, requires auth on foreground return.
Platform implementations: BiometricPrompt (Android), LAContext (iOS).
Rate limit: 429 responses parsed with Retry-After header, user-friendly
error messages in all 10 locales, retry plugin respects 429.
ErrorMessageParser updated for both iOS Swift and KMM.
Major infrastructure changes:
- BaseUITestCase: per-suite app termination via class setUp() prevents
stale state when parallel clones share simulators
- relaunchBetweenTests override for suites that modify login/onboarding state
- focusAndType: dedicated SecureTextField path handles iOS strong password
autofill suggestions (Choose My Own Password / Not Now dialogs)
- LoginScreenObject: tapSignUp/tapForgotPassword use scrollIntoView for
offscreen buttons instead of simple swipeUp
- Removed all coordinate taps from ForgotPasswordScreen, VerifyResetCodeScreen,
ResetPasswordScreen (Rule 3 compliance)
- Removed all usleep calls from screen objects (Rule 14 compliance)
App fixes exposed by tests:
- ContractorsListView: added onDismiss to sheet for list refresh after save
- AllTasksView: added Task.RefreshButton accessibility identifier
- AccessibilityIdentifiers: added Task.refreshButton
- DocumentsWarrantiesView: onDismiss handler for document list refresh
- Various form views: textContentType, submitLabel, onSubmit for keyboard flow
Test fixes:
- PasswordResetTests: handle auto-login after reset (app skips success screen)
- AuthenticatedUITestCase: refreshTasks() helper for kanban toolbar button
- All pre-login suites use relaunchBetweenTests for test independence
- Deleted dead code: AuthenticatedTestCase, SeededTestData, SeedTests,
CleanupTests, old Suite0/2/3, Suite1_RegistrationRebuildTests
10 remaining failures: 5 iOS strong password autofill (simulator env),
3 pull-to-refresh gesture on empty lists, 2 feature coverage edge cases.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix hex sizing to use aspectRatio layout instead of GeometryReader
- Remove hardcoded height estimation that caused gap between header and grid
- Set fill alpha to 0.3 and add 0.7 alpha stroke on colored hexagons
- Use 12 rows consistently
- Add forceRefresh parameter to getResidence
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Widget containerBackground now shows honeycomb texture when toggle is enabled
- Change PostHog personProfiles from .never to .always so anonymous sessions
and unique visitors are properly counted without identifying users
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds a toggleable honeycomb hexagonal grid overlay (matching the website pattern)
that can be enabled independently of any theme via the Appearance screen. Uses a
cached tiled UIImage approach consistent with the existing grain texture system.
Replaces the destructive refreshID-based theme switching (which destroyed all
NavigationStacks and dismissed sheets) with @Observable AppThemeSource. Color
resolution now happens through Swift's Observation framework, so all views using
Color.appPrimary etc. automatically re-render when the theme changes — no view
identity reset needed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Update DEV API URLs from treytartt.com to api.myhoneydue.com
- Add rounded corners to app icon in login, register, and onboarding screens
- Add 9 missing English translations in Localizable.xcstrings
- Fix property feature pills to use equal height for balanced layout
- Remove duplicate honeyDue user scheme
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Rename iosApp.xcodeproj → honeyDue.xcodeproj
- Rename Casera schemes → HoneyDue, HoneyDueExtension, HoneyDueUITests
- Split bundle IDs per-config: Debug=com.tt.honeyDue.dev, Release=com.tt.honeyDue
- Split app groups per-config: Debug=group.com.tt.honeyDue.dev, Release=group.com.tt.honeyDue
- Fix com.t-t. typo → com.tt. in test bundle IDs
- Add APP_GROUP_IDENTIFIER build setting with variable substitution in entitlements
- Replace all hardcoded app group strings in Swift with Info.plist runtime reads
- Remove stale PRODUCT_BUNDLE_IDENTIFIER from Config.xcconfig
- Update test plan container references
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Applies verified fixes from deep audit (concurrency, performance, security,
accessibility), standardizes CRUD form buttons to Add/Save pattern, removes
.drawingGroup() that broke search bar TextFields, and converts vulnerable
.sheet(isPresented:) + if-let patterns to safe presentation to prevent
blank white modals.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Rewrite ResidenceFormView to use standard Form/Section pattern matching TaskFormView
- Remove unused organic form components (OrganicFormSection, OrganicFormTextField, etc.)
- Fix DocumentFormView: NavigationView→NavigationStack, WarmGradientBackground→appBackgroundPrimary, listRowBackground→sectionBackground
- Add Required footer to residence name field and task title/property fields
- Remove redundant Required footers from pickers that always have values
- Fix grey priority dots on kanban cards by guarding PriorityBadge in DynamicTaskCard and TaskCard
- Fix empty frequency labels showing on task cards
- Fix contractor Maps URL building to filter empty strings
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fetch regional templates by ZIP during onboarding and display categorized
task suggestions (iOS + KMM shared layer)
- Fix multi-expand for task categories (toggle independently, not exclusive)
- Fix ScrollViewReader auto-scroll to expanded category sections
- Fix UUID stability bug: cache task categories to prevent ID regeneration
that caused silent task creation failures
- Fix stale data after onboarding: force refresh residences and tasks in
RootView onComplete callback
- Fix address formatting: show just ZIP code when city/state are empty
instead of showing ", 75028" with leading comma
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Completion animations: play user-selected animation on task card after completing,
with DataManager guard to prevent race condition during animation playback.
Works in both AllTasksView and ResidenceDetailView. Animation preference
persisted via @AppStorage and configurable from Settings.
- Subscription: add trial fields (trialStart, trialEnd, trialActive) and
subscriptionSource to model, cross-platform purchase guard, trial banner
in upgrade prompt, and platform-aware subscription management in profile.
- Analytics: disable PostHog SDK debug logging and remove console print
statements to reduce debug console noise.
- Documents: remove redundant nested do-catch blocks in ViewModel wrapper.
- Widgets: add debounced timeline reloads and thread-safe file I/O queue.
- Onboarding: fix animation leak on disappear, remove unused state vars.
- Remove unused files (ContentView, StateFlowExtensions, CustomView).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Eliminate NumberFormatters shared singleton data race; use local formatters
- Add reduceMotion checks to empty-state animations in 3 list views
- Wrap 68+ print() statements in #if DEBUG across push notification code
- Remove redundant .receive(on: DispatchQueue.main) in SubscriptionCache
- Remove redundant initializeLookups() call from iOSApp.init()
- Clean up StoreKitManager Task capture in listenForTransactions()
- Add memory warning observer to AuthenticatedImage cache
- Cache parseContent result in UpgradePromptView init
- Add DiskSpace and FileTimestamp API declarations to Privacy Manifest
- Add FIXME for analytics debug/production API key separation
- Use static formatter in PropertyHeaderCard
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PostHogAnalytics.shared.initialize() was calling the Kotlin actual object which
is a no-op on iOS. Replaced with AnalyticsManager.shared.configure() so the
PostHog SDK actually initializes. Also switched to anonymous-only person profiles.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
TokenManager.keychainDelegate was never set, so all Keychain reads/writes
failed silently. Tokens couldn't be persisted or loaded on app restart,
and TokenStorage.getToken() returned nil during task completion flows.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Change ApiConfig.CURRENT_ENV from LOCAL to DEV
- Add "Google Sign-In Error" and "Sign in with Google" to Localizable.xcstrings
- Reorder Xcode project build settings (cosmetic)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
27 new tests covering SubscriptionCacheWrapper: currentTier derivation,
shouldShowUpgradePrompt with per-resource limits and boundary conditions,
canShareResidence/canShareContractor gating, and deprecated prompt property.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>