Replace collectAsState() with collectAsStateWithLifecycle() so StateFlows
stop collecting when the host is in background — prevents memory/CPU leaks
on lifecycle transitions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Material 3 AutoMirrored variants flip correctly in Arabic/Hebrew.
Previous Icons.Default.ArrowBack pointed wrong direction in RTL.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Prep for parallel Android UI test suite build-out. These deps unblock
page-object tests using onNodeWithTag / createAndroidComposeRule and
cross-app permission-dialog interaction.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds acceptResidenceInvite / declineResidenceInvite to ResidenceApi
(POST /api/residences/{id}/invite/{accept|decline}) and exposes them via
APILayer. On accept success, myResidences is force-refreshed so the
newly-joined residence appears without a manual pull.
Wires NotificationActionReceiver's ACCEPT_INVITE / DECLINE_INVITE
handlers to the new APILayer calls, replacing the log-only TODOs left
behind by P4 Stream O. Notifications are now cleared only on API
success so a failed accept stays actionable.
Tests:
- ResidenceApiInviteTest covers correct HTTP method/path + error surfacing.
- NotificationActionReceiverTest invite cases updated to assert the new
APILayer calls (were previously asserting the log-only path).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Port iOS TaskAnimations.swift specs (completion checkmark, card transitions,
priority pulse) + AnimationTestingView as dev screen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stream M replaced the widget's task-complete path with CompleteTaskAction +
WidgetActionProcessor. Grepping confirmed the only references to
WidgetTaskActionReceiver were its own class file and the manifest entry.
Remove both.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move the legacy MyFirebaseMessagingService.onNewToken() device-registration
path onto the new iOS-parity FcmService. The legacy service is already
unwired from the manifest MESSAGING_EVENT filter; this removes its last
functional responsibility. Behaviour preserved: auth-token guard,
DeviceRegistrationRequest with ANDROID_ID + Build.MODEL, log-only on error.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Navigation wiring (per follow-ups flagged by Streams G/H/I):
- Add TaskTemplatesBrowserRoute (new) + App.kt composable<TaskTemplatesBrowserRoute>
- Wire composable<TaskSuggestionsRoute> (declared by Stream H but unwired)
- Wire composable<AddTaskWithResidenceRoute> (declared by Stream I but unwired)
MainActivity.onCreate now calls HapticsInit.install(applicationContext) so the
Vibrator fallback path works on non-View call-sites (flagged by Stream S).
Deferred cleanup (tracked, not done here):
- Port push-token registration from legacy MyFirebaseMessagingService.kt to
new FcmService (Stream N TODO).
- Remove legacy WidgetTaskActionReceiver + manifest entry (Stream M flag).
- Residence invite accept/decline APILayer methods (Stream O TODO).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Port iOS AddTaskWithResidenceView. Residence pre-selected via constructor
param, form validation, submit -> APILayer.createTask(residenceId attached).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
BiometricPrompt wrapper with 3-strike lockout + NO_HARDWARE bypass.
BiometricLockScreen with auto-prompt on mount + PIN fallback after 3 failures.
PIN wiring marked TODO for secure-storage follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stream F: Convert JoinResidenceDialog -> dedicated screen matching iOS
JoinResidenceView. Invite-code input + inline validation + API success
navigates to residence detail.
Stream U fix: coil3 3.0.4 doesn't ship ColorImage (added in 3.1.0). Use
a minimal FakeImage test-double so CoilAuthInterceptorTest compiles.
Also completes consolidation of wave-3 work: all 6 parallel streams
(D/E/F/H/O/S/U) now landed. Full unit suite green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Port iOS TaskSuggestionsView as a standalone route reachable outside
onboarding. Uses shared suggestions API + accept/skip analytics in
non-onboarding variant.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stream M's WidgetActionProcessorTest.kt references this predicate but
Stream J's initial repo only exposed mark/clear mutators. Trivial addition:
reads the pending-completion set and checks membership.
Unblocks :composeApp:testDebugUnitTest (now green across all streams).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Glance ActionCallback wires to WidgetActionProcessor: premium marks pending +
calls API + refreshes; free tier opens paywall deep link instead. Idempotent,
rollback-safe on API failure.
Also fixes a one-line compile error in WidgetTaskActionReceiver.kt where
updateAllWidgets() had been removed by Stream L — swapped for forceRefresh()
so the build stays green. The legacy receiver is now redundant (replaced by
CompleteTaskAction) but deletion is deferred to a Stream K follow-up so the
AndroidManifest entry can be removed in the same commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Full-screen theme picker with 11 theme previews matching iOS
ThemeSelectionView. Live preview on tap. Old dialog deleted and
call-sites migrated to the new route.
Tests use the state-logic fallback pattern (plain kotlin.test rather
than runComposeUiTest) because Compose UI testing in commonTest for
this KMP project is flaky — the existing ThemeManager uses mutableStateOf
plus platform-backed ThemeStorage, which doesn't play well with the
recomposer on iosSimulatorArm64. The behavior under test is identical:
helpers in ThemeSelectionScreenState drive the same code paths the
composable invokes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
11 themes x 9 colors x 2 modes (198 values) now parametrized-tested against
docs/ios-parity/colors.json as ground truth. Typography scale matches iOS
dynamic-type defaults.
118 of 198 color values were off before — mostly off-by-one RGB rounding
errors introduced during a prior hand-copy from iOS. Default TextSecondary
(light+dark) also lost its 0x99 alpha channel — now restored via
Color(0xAARRGGBB) packed literals. Added SansSerif fontFamily and source
citations on every Typography size so the iOS dynamic-type mapping is
explicit.
Tests: ThemeColorsTest (4), TypographyTest (5), SpacingTest (1) — all
green. `everyColorMatchesIosGroundTruth` walks the embedded JSON and
asserts 198 hex values match exactly.
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.
- DELETE /api/auth/account/ API call in AuthApi + APILayer
- authProvider field on User model for email vs social auth detection
- DeleteAccountDialog with password (email) or "type DELETE" (social) confirmation
- Red "Delete Account" card on ProfileScreen
- Navigation wired in App.kt (clears data, returns to login)
- 10 i18n strings in strings.xml
- ViewModel unit tests for delete account state
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>
Backend now returns Document directly instead of wrapped
DocumentActionResponse. Remove unused DocumentActionResponse class.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>