Remove docs, guides, and readmes relocated to old_files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-03-07 07:09:37 -06:00
parent d3b6b14e78
commit 8941d4f458
33 changed files with 0 additions and 9885 deletions

View File

@@ -1,255 +0,0 @@
# Environment Configuration Guide
This guide explains how to easily switch between local development and the dev server when developing the HoneyDue iOS and Android apps.
## Quick Start
**To switch environments, change ONE line in `ApiConfig.kt`:**
```kotlin
// File: composeApp/src/commonMain/kotlin/com/honeydue/shared/network/ApiConfig.kt
object ApiConfig {
// ⚠️ CHANGE THIS LINE ⚠️
val CURRENT_ENV = Environment.LOCAL // or Environment.DEV
}
```
## Environment Options
### 1. Local Development (`Environment.LOCAL`)
**Use this when:**
- Running the Django API on your local machine
- Debugging API changes
- Working offline
**Connects to:**
- **Android**: `http://10.0.2.2:8000/api` (Android emulator localhost alias)
- **iOS**: `http://127.0.0.1:8000/api` (iOS simulator localhost)
**Setup:**
```kotlin
val CURRENT_ENV = Environment.LOCAL
```
**Requirements:**
- Django API running on `http://localhost:8000`
- Use `./dev.sh` to start the API with auto-reload
### 2. Dev Server (`Environment.DEV`)
**Use this when:**
- Testing against the deployed server
- You don't have the API running locally
- Testing with real data
**Connects to:**
- **Both platforms**: `https://honeyDue.treytartt.com/api`
**Setup:**
```kotlin
val CURRENT_ENV = Environment.DEV
```
**Requirements:**
- Internet connection
- Dev server must be running and accessible
## Step-by-Step Instructions
### Switching to Local Development
1. **Start your local API:**
```bash
cd honeyDueAPI
./dev.sh
```
2. **Update `ApiConfig.kt`:**
```kotlin
val CURRENT_ENV = Environment.LOCAL
```
3. **Rebuild the app:**
- **Android**: Sync Gradle and run
- **iOS**: Clean build folder (⇧⌘K) and run
4. **Verify in logs:**
```
🌐 API Client initialized
📍 Environment: Local (10.0.2.2:8000)
🔗 Base URL: http://10.0.2.2:8000/api
```
### Switching to Dev Server
1. **Update `ApiConfig.kt`:**
```kotlin
val CURRENT_ENV = Environment.DEV
```
2. **Rebuild the app:**
- **Android**: Sync Gradle and run
- **iOS**: Clean build folder (⇧⌘K) and run
3. **Verify in logs:**
```
🌐 API Client initialized
📍 Environment: Dev Server (honeyDue.treytartt.com)
🔗 Base URL: https://honeyDue.treytartt.com/api
```
## Platform-Specific Localhost Addresses
The localhost addresses are automatically determined by platform:
| Platform | Localhost Address | Reason |
|----------|-------------------|--------|
| Android Emulator | `10.0.2.2` | Special alias for host machine's localhost |
| iOS Simulator | `127.0.0.1` | Standard localhost (simulator shares network with host) |
| Android Device | Your machine's IP | Must manually set in `ApiClient.android.kt` |
| iOS Device | Your machine's IP | Must manually set in `ApiClient.ios.kt` |
### Testing on Physical Devices
If you need to test on a physical device with local API:
1. **Find your machine's IP address:**
```bash
# macOS/Linux
ifconfig | grep "inet "
# Look for something like: 192.168.1.xxx
```
2. **Update platform-specific file:**
**Android** (`ApiClient.android.kt`):
```kotlin
actual fun getLocalhostAddress(): String = "192.168.1.xxx"
```
**iOS** (`ApiClient.ios.kt`):
```kotlin
actual fun getLocalhostAddress(): String = "192.168.1.xxx"
```
3. **Ensure your device is on the same WiFi network as your machine**
4. **Update Django's `ALLOWED_HOSTS`:**
```python
# honeyDueAPI/honeyDue/settings.py
ALLOWED_HOSTS = ['localhost', '127.0.0.1', '192.168.1.xxx']
```
## File Structure
```
HoneyDueKMM/composeApp/src/
├── commonMain/kotlin/com/honeydue/shared/network/
│ ├── ApiConfig.kt # ⭐ TOGGLE ENVIRONMENT HERE
│ └── ApiClient.kt # Uses ApiConfig
├── androidMain/kotlin/com/honeydue/shared/network/
│ └── ApiClient.android.kt # Android localhost: 10.0.2.2
└── iosMain/kotlin/com/honeydue/shared/network/
└── ApiClient.ios.kt # iOS localhost: 127.0.0.1
```
## Troubleshooting
### Android: "Unable to connect to server"
**Problem:** Android can't reach localhost
**Solutions:**
1. Use `10.0.2.2` instead of `localhost` or `127.0.0.1`
2. Make sure API is running on `0.0.0.0:8000`, not just `127.0.0.1:8000`
3. Check that `CURRENT_ENV = Environment.LOCAL`
### iOS: "Connection refused"
**Problem:** iOS simulator can't connect
**Solutions:**
1. Use `127.0.0.1` for iOS simulator
2. Make sure Django is running
3. Try accessing `http://127.0.0.1:8000/api` in Safari on your Mac
4. Check firewall settings
### Dev Server: SSL/Certificate errors
**Problem:** HTTPS connection issues
**Solutions:**
1. Verify server is accessible: `curl https://honeyDue.treytartt.com/api`
2. Check that SSL certificate is valid
3. Make sure you're using `https://` not `http://`
### Changes not taking effect
**Problem:** Environment change not working
**Solutions:**
1. **Clean and rebuild:**
- Android: Build → Clean Project, then rebuild
- iOS: Product → Clean Build Folder (⇧⌘K)
2. **Invalidate caches:**
- Android Studio: File → Invalidate Caches
3. **Check logs for current environment:**
- Look for `🌐 API Client initialized` log message
## Best Practices
1. **Commit with LOCAL:** Always commit code with `Environment.LOCAL` so teammates use their local API by default
2. **Document API changes:** If you change the API, update both local and dev server
3. **Test both environments:** Before deploying, test with both LOCAL and DEV
4. **Use dev server for demos:** Switch to DEV when showing the app to others
5. **Keep localhost addresses:** Don't commit IP addresses, use the platform-specific aliases
## Quick Reference
| Action | Command/File |
|--------|--------------|
| Toggle environment | Edit `ApiConfig.kt` → `CURRENT_ENV` |
| Start local API | `cd honeyDueAPI && ./dev.sh` |
| Android localhost | `10.0.2.2:8000` |
| iOS localhost | `127.0.0.1:8000` |
| Dev server | `https://honeyDue.treytartt.com` |
| View current env | Check app logs for `🌐` emoji |
## Example Workflow
**Morning: Start work**
```kotlin
// ApiConfig.kt
val CURRENT_ENV = Environment.LOCAL // ✅ Use local API
```
```bash
cd honeyDueAPI
./dev.sh # Start local server
# Work on features...
```
**Testing: Check remote data**
```kotlin
// ApiConfig.kt
val CURRENT_ENV = Environment.DEV // ✅ Use dev server
# Rebuild app and test
```
**Before commit**
```kotlin
// ApiConfig.kt
val CURRENT_ENV = Environment.LOCAL // ✅ Reset to LOCAL
git add .
git commit -m "Add new feature"
```
---
**Need help?** Check the logs when the app starts - they'll tell you exactly which environment and URL is being used!

View File

@@ -1,40 +0,0 @@
# Error Handling Guide
This guide explains how to implement consistent error handling with retry/cancel dialogs across Android and iOS apps.
## Android (Compose)
### Components Available
1. **ErrorDialog** - Reusable Material3 AlertDialog with retry/cancel buttons
2. **ApiResultHandler** - Composable that automatically handles ApiResult states
3. **HandleErrors()** - Extension function for ApiResult states
### Usage Examples
See full documentation in the file for complete examples of:
- Using ApiResultHandler for data loading screens
- Using HandleErrors() extension for create/update/delete operations
- Using ErrorDialog directly for custom scenarios
## iOS (SwiftUI)
### Components Available
1. **ErrorAlertModifier** - View modifier that shows error alerts
2. **ViewStateHandler** - View that handles loading/error/success states
3. **handleErrors()** - View extension for automatic error monitoring
### Usage Examples
See full documentation for examples of each approach.
## Files Reference
**Android:**
- `composeApp/src/commonMain/kotlin/com/example/honeydue/ui/components/ErrorDialog.kt`
- `composeApp/src/commonMain/kotlin/com/example/honeydue/ui/components/ApiResultHandler.kt`
**iOS:**
- `iosApp/iosApp/Helpers/ErrorAlertModifier.swift`
- `iosApp/iosApp/Helpers/ViewStateHandler.swift`

View File

@@ -1,64 +0,0 @@
# HoneyDue KMM - Quick Start
## 🚀 Switch API Environment
**File:** `composeApp/src/commonMain/kotlin/com/honeydue/shared/network/ApiConfig.kt`
```kotlin
object ApiConfig {
val CURRENT_ENV = Environment.LOCAL // ⬅️ CHANGE THIS
}
```
### Options:
- **`Environment.LOCAL`** → Your local API (localhost)
- **`Environment.DEV`** → Dev server (https://honeyDue.treytartt.com)
### After Changing:
1. **Android**: Sync Gradle and run
2. **iOS**: Clean Build Folder (⇧⌘K) and run
### Verify in Logs:
```
🌐 API Client initialized
📍 Environment: Local (10.0.2.2:8000)
🔗 Base URL: http://10.0.2.2:8000/api
```
---
## 📱 Run the Apps
### Android
```bash
cd HoneyDueKMM
./gradlew :composeApp:installDebug
```
### iOS
```bash
cd HoneyDueKMM/iosApp
open iosApp.xcodeproj
# Run in Xcode
```
---
## 🔧 Start Local API
```bash
cd honeyDueAPI
./dev.sh # Auto-reload on code changes
```
---
## 📚 Full Guides
- **Environment Setup**: `ENVIRONMENT_SETUP.md`
- **Workspace Overview**: `../WORKSPACE_OVERVIEW.md`
- **API Deployment**: `../honeyDueAPI/DOKKU_SETUP_GUIDE.md`
---
**That's it!** Change one line to toggle between local and remote development. ✨

View File

@@ -1,142 +0,0 @@
# Task Disk Caching
## Overview
All user tasks are automatically cached to disk for offline access. This provides a seamless experience even when the network is unavailable.
## How It Works
### On App Startup (After Login)
```kotlin
LookupsRepository.initialize()
```
1. **Loads cached tasks from disk immediately** - Instant access to previously fetched tasks
2. **Fetches fresh data from API** - Updates with latest data when online
3. **Saves to disk** - Overwrites cache with fresh data
### Cache Flow
```
App Start
Load from Disk Cache (Instant offline access)
Fetch from API (When online)
Update Memory + Disk Cache
UI Updates Automatically
```
## Platform Storage
### Android
- **Location**: SharedPreferences
- **File**: `honeydue_cache`
- **Key**: `cached_tasks`
- **Format**: JSON string
### iOS
- **Location**: NSUserDefaults
- **Key**: `cached_tasks`
- **Format**: JSON string
### JVM/Desktop
- **Location**: Java Preferences
- **Node**: `com.honeydue.cache`
- **Key**: `cached_tasks`
- **Format**: JSON string
## Usage
### Accessing Cached Tasks
**Kotlin/Android:**
```kotlin
val tasks by LookupsRepository.allTasks.collectAsState()
// Tasks are available immediately from cache, updated when API responds
```
**Swift/iOS:**
```swift
@ObservedObject var lookupsManager = LookupsManager.shared
// Access via: lookupsManager.allTasks
```
### Manual Operations
```kotlin
// Refresh from API and update cache
LookupsRepository.refresh()
// Clear cache (called automatically on logout)
LookupsRepository.clear()
```
## Benefits
**Offline Access** - Tasks available without network
**Fast Startup** - Instant task display from disk cache
**Automatic Sync** - Fresh data fetched and cached when online
**Seamless UX** - Users don't notice when offline
**Battery Friendly** - Reduces unnecessary API calls
## Cache Lifecycle
### On Login
- Loads tasks from disk (if available)
- Fetches from API
- Updates disk cache
### During Session
- Tasks served from memory (StateFlow)
- Can manually refresh with `LookupsRepository.refresh()`
### On Logout
- Memory cache cleared
- **Disk cache cleared** (for security)
### On App Restart
- Cycle repeats with cached data from disk
## Implementation Files
**Common:**
- `TaskCacheStorage.kt` - Unified cache interface
- `TaskCacheManager.kt` - Platform-specific interface
**Platform Implementations:**
- `TaskCacheManager.android.kt` - SharedPreferences
- `TaskCacheManager.ios.kt` - NSUserDefaults
- `TaskCacheManager.jvm.kt` - Java Preferences
**Repository:**
- `LookupsRepository.kt` - Cache orchestration
## Storage Size
Tasks are stored as JSON. Approximate sizes:
- 100 tasks ≈ 50-100 KB
- 1000 tasks ≈ 500 KB - 1 MB
Storage is minimal and well within platform limits.
## Error Handling
If cache read fails:
- Logs error to console
- Returns `null`
- Falls back to API fetch
If cache write fails:
- Logs error to console
- Continues normally
- Retry on next API fetch
## Security Notes
- Cache is cleared on logout
- Uses platform-standard secure storage
- No sensitive authentication data cached
- Only task data (titles, descriptions, status, etc.)

View File

@@ -1,259 +0,0 @@
# Task Summary Component Usage
## Overview
The `TaskSummaryCard` component displays task statistics dynamically based on backend data. The backend provides all metadata (icons, colors, display names), making the UI fully data-driven.
## Backend Structure
The backend returns task summaries in this format:
```json
{
"total": 25,
"categories": [
{
"name": "overdue_tasks",
"display_name": "Overdue",
"icons": {
"ios": "exclamationmark.triangle",
"android": "Warning",
"web": "exclamation-triangle"
},
"color": "#FF3B30",
"count": 3
},
{
"name": "current_tasks",
"display_name": "Current",
"icons": {
"ios": "calendar",
"android": "CalendarToday",
"web": "calendar"
},
"color": "#007AFF",
"count": 8
},
// ... more categories
]
}
```
## Model Classes
The app models are defined in `Residence.kt`:
```kotlin
@Serializable
data class TaskCategoryIcon(
val ios: String,
val android: String,
val web: String
)
@Serializable
data class TaskCategory(
val name: String,
@SerialName("display_name") val displayName: String,
val icons: TaskCategoryIcon,
val color: String,
val count: Int
)
@Serializable
data class TaskSummary(
val total: Int,
val categories: List<TaskCategory>
)
```
## Usage Examples
### Example 1: Show All Categories
```kotlin
@Composable
fun ResidenceDetailScreen(residence: ResidenceWithTasks) {
Column {
// Other residence details...
TaskSummaryCard(
taskSummary = residence.taskSummary
)
}
}
```
### Example 2: Show Only Specific Categories
For the main residence view, you might want to show only certain categories:
```kotlin
@Composable
fun ResidenceCard(residence: ResidenceSummary) {
Card {
Column {
Text(text = residence.name)
// Only show overdue, current, and in_progress
TaskSummaryCard(
taskSummary = residence.taskSummary,
visibleCategories = listOf(
"overdue_tasks",
"current_tasks",
"in_progress_tasks"
)
)
}
}
}
```
### Example 3: Custom Styling
```kotlin
TaskSummaryCard(
taskSummary = residence.taskSummary,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
visibleCategories = listOf("overdue_tasks", "current_tasks")
)
```
## Available Categories
The backend defines these categories (in `task/constants.py`):
1. **overdue_tasks** - Red (#FF3B30) - Tasks past their due date
2. **current_tasks** - Blue (#007AFF) - Tasks due within threshold (29 days)
3. **in_progress_tasks** - Orange (#FF9500) - Tasks being worked on
4. **backlog_tasks** - Purple (#5856D6) - Tasks beyond current threshold
5. **done_tasks** - Green (#34C759) - Completed tasks
6. **archived_tasks** - Gray (#8E8E93) - Archived tasks
## Customization
### Filtering Categories
Use the `visibleCategories` parameter to control which categories appear:
```kotlin
// Show only active task categories
TaskSummaryCard(
taskSummary = taskSummary,
visibleCategories = listOf(
"overdue_tasks",
"current_tasks",
"in_progress_tasks",
"backlog_tasks"
)
)
```
### Styling
The component uses Material Design 3 theming. Customize via modifier:
```kotlin
TaskSummaryCard(
taskSummary = taskSummary,
modifier = Modifier
.padding(horizontal = 16.dp)
.shadow(4.dp, RoundedCornerShape(12.dp))
)
```
## iOS Implementation
For iOS (SwiftUI), create a similar component:
```swift
struct TaskSummaryCard: View {
let taskSummary: TaskSummary
let visibleCategories: [String]?
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Tasks")
.font(.headline)
ForEach(filteredCategories, id: \.name) { category in
TaskCategoryRow(category: category)
}
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(radius: 2)
}
var filteredCategories: [TaskCategory] {
if let visible = visibleCategories {
return taskSummary.categories.filter { visible.contains($0.name) }
}
return taskSummary.categories
}
}
struct TaskCategoryRow: View {
let category: TaskCategory
var body: some View {
HStack {
// Use SF Symbol from category.icons.ios
Image(systemName: category.icons.ios)
.foregroundColor(Color(hex: category.color))
Text(category.displayName)
.font(.body)
Spacer()
Text("\(category.count)")
.font(.headline)
.foregroundColor(.white)
.padding(.horizontal, 12)
.padding(.vertical, 4)
.background(Color(hex: category.color))
.cornerRadius(12)
}
.padding(12)
.background(Color(hex: category.color).opacity(0.1))
.cornerRadius(8)
}
}
```
## Benefits
**Data-Driven**: All icons, colors, and names come from backend
**Single Source of Truth**: Change values in `task/constants.py` to update everywhere
**Flexible**: Easy to show different categories in different screens
**Consistent**: Same categorization logic across all platforms
**Maintainable**: No hardcoded UI values in app code
## Migration from Old TaskSummary
If you have existing code using the old TaskSummary structure:
**Old:**
```kotlin
Text("Overdue: ${taskSummary.overdue}")
Text("Pending: ${taskSummary.pending}")
Text("Completed: ${taskSummary.completed}")
```
**New:**
```kotlin
TaskSummaryCard(
taskSummary = taskSummary,
visibleCategories = listOf("overdue_tasks", "current_tasks", "done_tasks")
)
```
Or access categories programmatically:
```kotlin
val overdueCount = taskSummary.categories
.find { it.name == "overdue_tasks" }?.count ?: 0
```

View File

@@ -1,146 +0,0 @@
# String Localization Migration TODO
This file tracks the remaining work to migrate hardcoded strings to Compose Resources (`composeApp/src/commonMain/composeResources/values/strings.xml`).
## Completed ✅
### High Priority Files (Done)
- [x] `DocumentFormScreen.kt` - 48 strings migrated
- [x] `AddTaskDialog.kt` - 28 strings migrated
## Remaining Work
### Priority 1: Dialogs with Many Strings
#### AddContractorDialog.kt (~25 strings)
Location: `composeApp/src/commonMain/kotlin/com/example/honeydue/ui/components/AddContractorDialog.kt`
Strings to migrate:
- Dialog title: "Add Contractor"
- Field labels: Name *, Company, Phone, Email, Specialty, Notes, Website, Address
- Validation errors: "Name is required"
- Buttons: "Create", "Cancel"
#### CompleteTaskDialog.kt (~22 strings)
Location: `composeApp/src/commonMain/kotlin/com/example/honeydue/ui/components/CompleteTaskDialog.kt`
Strings to migrate:
- Dialog title: "Complete Task"
- Field labels: Notes, Actual Cost, Completion Date
- Photo section: "Photos", "Camera", "Gallery", "Remove"
- Buttons: "Complete", "Cancel"
- Validation messages
### Priority 2: Import/Share Dialogs (~14 strings)
#### ContractorImportDialog.kt (~7 strings)
Location: `composeApp/src/commonMain/kotlin/com/example/honeydue/ui/components/ContractorImportDialog.kt`
#### ResidenceImportDialog.kt (~7 strings)
Location: `composeApp/src/commonMain/kotlin/com/example/honeydue/ui/components/ResidenceImportDialog.kt`
### Priority 3: Task Components (~14 strings)
#### TaskActionButtons.kt (~7 strings)
Location: `composeApp/src/commonMain/kotlin/com/example/honeydue/ui/components/task/TaskActionButtons.kt`
#### TaskCard.kt (~7 strings)
Location: `composeApp/src/commonMain/kotlin/com/example/honeydue/ui/components/task/TaskCard.kt`
### Priority 4: Other Dialogs (~10 strings)
#### JoinResidenceDialog.kt (~7 strings)
Location: `composeApp/src/commonMain/kotlin/com/example/honeydue/ui/components/JoinResidenceDialog.kt`
#### ManageUsersDialog.kt (~2 strings)
Location: `composeApp/src/commonMain/kotlin/com/example/honeydue/ui/components/ManageUsersDialog.kt`
#### TaskTemplatesBrowserSheet.kt (~3 strings)
Location: `composeApp/src/commonMain/kotlin/com/example/honeydue/ui/components/TaskTemplatesBrowserSheet.kt`
### Priority 5: Smaller Components (~15 strings total)
Files with 1-3 hardcoded strings each:
- `InfoCard.kt`
- `FeatureComparisonDialog.kt`
- `ThemePickerDialog.kt`
- `StandardCard.kt`
- `CompactCard.kt`
- `ApiResultHandler.kt`
- `DocumentCard.kt`
- `DocumentStates.kt`
- `CompletionHistorySheet.kt`
- `DocumentDetailScreen.kt`
- `EditTaskScreen.kt`
- `MainScreen.kt`
- `UpgradePromptDialog.kt`
- `VerifyEmailScreen.kt`
- `VerifyResetCodeScreen.kt`
- `UpgradeFeatureScreen.kt`
- `ResidenceFormScreen.kt`
## How to Migrate Strings
### 1. Add import to the file:
```kotlin
import honeydue.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource
```
### 2. Add string to strings.xml:
```xml
<string name="component_field_label">Field Label</string>
```
### 3. Replace hardcoded string:
```kotlin
// Before
Text("Field Label")
// After
Text(stringResource(Res.string.component_field_label))
```
### 4. For strings with parameters:
```xml
<string name="items_count">%1$d items</string>
```
```kotlin
Text(stringResource(Res.string.items_count, count))
```
### 5. For strings used in onClick handlers:
Define outside the lambda (stringResource is @Composable):
```kotlin
val errorMessage = stringResource(Res.string.error_message)
Button(onClick = {
// Use errorMessage here
showError = errorMessage
})
```
## Naming Convention
Use this pattern for string names:
- `{component}_{field}` - e.g., `contractor_name_label`
- `{component}_{action}` - e.g., `contractor_create`
- `{component}_{error}` - e.g., `contractor_name_error`
Existing prefixes in strings.xml:
- `auth_` - Authentication screens
- `properties_` - Residence/property screens
- `tasks_` - Task screens and components
- `contractors_` - Contractor screens
- `documents_` - Document screens
- `profile_` - Profile screens
- `common_` - Shared strings (Cancel, OK, Back, etc.)
## Testing
After migrating strings, run:
```bash
./gradlew :composeApp:compileDebugKotlinAndroid
```
Build should complete successfully with only deprecation warnings.

View File

@@ -1,276 +0,0 @@
# Android Subscription & Upgrade UI Parity Plan
## Goal
Bring Android subscription/upgrade functionality and UX to match iOS implementation:
1. Show full inline paywall (not teaser + dialog)
2. Implement Google Play Billing integration
3. Disable FAB when upgrade screen is showing
## Current State
### iOS (Reference)
- `UpgradeFeatureView` shows full inline paywall with:
- Promo content card with feature bullets
- Subscription product buttons with real pricing
- Purchase flow via StoreKit 2
- "Compare Free vs Pro" and "Restore Purchases" links
- Add button disabled/grayed when upgrade showing
- `StoreKitManager` fully implemented
### Android (Current)
- `UpgradeFeatureScreen` shows simple teaser → opens `UpgradePromptDialog`
- FAB always enabled
- `BillingManager` is a stub (no real billing)
- No Google Play Billing dependency
---
## Implementation Plan
### Phase 1: Add Google Play Billing Dependency
**Files to modify:**
- `gradle/libs.versions.toml` - Add billing library version
- `composeApp/build.gradle.kts` - Add dependency to androidMain
```toml
# libs.versions.toml
billing = "7.1.1"
[libraries]
google-billing = { module = "com.android.billingclient:billing-ktx", version.ref = "billing" }
```
```kotlin
# build.gradle.kts - androidMain.dependencies
implementation(libs.google.billing)
```
---
### Phase 2: Implement BillingManager
**File:** `composeApp/src/androidMain/kotlin/com/example/honeydue/platform/BillingManager.kt`
Replace stub implementation with full Google Play Billing:
```kotlin
class BillingManager private constructor(private val context: Context) {
// Product IDs (match Google Play Console)
private val productIDs = listOf(
"com.tt.honeyDue.pro.monthly",
"com.tt.honeyDue.pro.annual"
)
// BillingClient instance
private var billingClient: BillingClient
// StateFlows for UI
val products = MutableStateFlow<List<ProductDetails>>(emptyList())
val purchasedProductIDs = MutableStateFlow<Set<String>>(emptySet())
val isLoading = MutableStateFlow(false)
val purchaseError = MutableStateFlow<String?>(null)
// Key methods to implement:
- startConnection() - Connect to Google Play
- loadProducts() - Query subscription products
- purchase(activity, productDetails) - Launch purchase flow
- restorePurchases() - Query purchase history
- verifyPurchaseWithBackend() - Call SubscriptionApi.verifyAndroidPurchase()
- acknowledgePurchase() - Required by Google
- listenForPurchases() - PurchasesUpdatedListener
```
**Key implementation details:**
1. Initialize BillingClient with PurchasesUpdatedListener
2. Handle billing connection state (retry on disconnect)
3. Query products using QueryProductDetailsParams with ProductType.SUBS
4. Launch purchase flow with BillingFlowParams
5. Process purchase results and verify with backend
6. Acknowledge purchases (required or they refund after 3 days)
7. Update SubscriptionCache after successful verification
---
### Phase 3: Update UpgradeFeatureScreen
**File:** `composeApp/src/commonMain/kotlin/com/example/honeydue/ui/subscription/UpgradeFeatureScreen.kt`
Transform from teaser+dialog to full inline paywall matching iOS:
**Current structure:**
- Icon, title, message, badge
- Button opens UpgradePromptDialog
**New structure:**
```kotlin
@Composable
fun UpgradeFeatureScreen(
triggerKey: String,
icon: ImageVector,
onNavigateBack: () -> Unit,
billingManager: BillingManager? = null // Android-only, null on other platforms
) {
// ScrollView with:
// 1. Star icon (accent gradient)
// 2. Title + message from triggerData
// 3. PromoContentCard - feature bullets from triggerData.promoHtml
// 4. SubscriptionProductButtons - show real products with pricing
// 5. "Compare Free vs Pro" button
// 6. "Restore Purchases" button
// 7. Error display if any
}
```
**New components to add:**
- `PromoContentCard` - Parse and display promo HTML as composable
- `SubscriptionProductButton` - Display product with name, price, optional savings badge
---
### Phase 4: Create Android-Specific Product Display
**File:** `composeApp/src/androidMain/kotlin/com/example/honeydue/ui/subscription/SubscriptionProductButton.kt`
```kotlin
@Composable
fun SubscriptionProductButton(
productDetails: ProductDetails,
isSelected: Boolean,
isProcessing: Boolean,
onSelect: () -> Unit
) {
// Display:
// - Product name (e.g., "HoneyDue Pro Monthly")
// - Price from subscriptionOfferDetails
// - "Save X%" badge for annual
// - Loading indicator when processing
}
```
**Helper function for savings calculation:**
```kotlin
fun calculateAnnualSavings(monthly: ProductDetails, annual: ProductDetails): Int?
```
---
### Phase 5: Disable FAB When Upgrade Showing
**Files to modify:**
- `composeApp/src/commonMain/kotlin/com/example/honeydue/ui/screens/ContractorsScreen.kt`
- `composeApp/src/commonMain/kotlin/com/example/honeydue/ui/screens/DocumentsScreen.kt`
**Changes:**
```kotlin
// In ContractorsScreen
val shouldShowUpgradePrompt = SubscriptionHelper.shouldShowUpgradePromptForContractors().allowed
// Update FAB
FloatingActionButton(
onClick = { if (!shouldShowUpgradePrompt) showAddDialog = true },
containerColor = if (shouldShowUpgradePrompt)
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f)
else
MaterialTheme.colorScheme.primary,
contentColor = if (shouldShowUpgradePrompt)
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f)
else
MaterialTheme.colorScheme.onPrimary
) {
Icon(Icons.Default.Add, "Add contractor")
}
// Add .alpha() modifier or enabled state
```
Same pattern for DocumentsScreen.
---
### Phase 6: Initialize BillingManager in MainActivity
**File:** `composeApp/src/androidMain/kotlin/com/example/honeydue/MainActivity.kt`
```kotlin
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Existing initializations...
TokenStorage.initialize(...)
ThemeStorage.initialize(...)
ThemeManager.initialize()
// Add BillingManager initialization
val billingManager = BillingManager.getInstance(applicationContext)
billingManager.startConnection(
onSuccess = { billingManager.loadProducts() },
onError = { error -> Log.e("Billing", "Connection failed: $error") }
)
setContent {
// Pass billingManager to composables that need it
App(billingManager = billingManager)
}
}
```
---
### Phase 7: Wire Purchase Flow End-to-End
**Integration points:**
1. **UpgradeFeatureScreen** observes BillingManager.products StateFlow
2. User taps product → calls BillingManager.purchase(activity, productDetails)
3. **BillingManager** launches Google Play purchase UI
4. On success → calls SubscriptionApi.verifyAndroidPurchase()
5. Backend verifies with Google → updates user's subscription tier
6. **BillingManager** calls SubscriptionApi.getSubscriptionStatus()
7. Updates **SubscriptionCache** with new status
8. UI recomposes, upgrade screen disappears, FAB becomes enabled
---
## Files Summary
| File | Action |
|------|--------|
| `gradle/libs.versions.toml` | Add billing version |
| `composeApp/build.gradle.kts` | Add billing dependency |
| `BillingManager.kt` | Full rewrite with real billing |
| `UpgradeFeatureScreen.kt` | Transform to inline paywall |
| `ContractorsScreen.kt` | Disable FAB when upgrade showing |
| `DocumentsScreen.kt` | Disable FAB when upgrade showing |
| `MainActivity.kt` | Initialize BillingManager |
---
## Reference Files (iOS Implementation)
These files show the iOS implementation to mirror:
- `iosApp/iosApp/Subscription/StoreKitManager.swift` - Full billing manager
- `iosApp/iosApp/Subscription/UpgradeFeatureView.swift` - Inline paywall UI
- `iosApp/iosApp/Subscription/UpgradePromptView.swift` - PromoContentView, SubscriptionProductButton
---
## Testing Checklist
- [ ] Products load from Google Play Console
- [ ] Purchase flow launches correctly
- [ ] Purchase verification with backend works
- [ ] SubscriptionCache updates after purchase
- [ ] FAB disabled when upgrade prompt showing
- [ ] FAB enabled after successful purchase
- [ ] Restore purchases works
- [ ] Error states display correctly
---
## Notes
- Product IDs must match Google Play Console: `com.tt.honeyDue.pro.monthly`, `com.tt.honeyDue.pro.annual`
- Backend endpoint `POST /subscription/verify-android/` already exists in SubscriptionApi
- Testing requires Google Play Console setup with test products
- Use Google's test cards for sandbox testing

View File

@@ -1,89 +0,0 @@
# Onboarding Statistics Sources
This document provides citations for the statistics used in the honeyDue iOS onboarding screens.
## Statistics Used in the App
### 1. "$6,000+ spent yearly on repairs that could've been prevented"
**Sources:**
- [Real Estate Witch Survey](https://www.realestatewitch.com/) - Homeowners spend an average of $6,087 every year on unexpected fixes
- [CNBC / Hippo Insurance Report](https://www.cnbc.com/2023/01/05/homeowners-spent-up-to-6000-average-on-repairs-maintenance-in-2022.html) - In 2022, homeowners spent an average of $6,000 on maintenance and repairs
- [Hippo Insurance Report](https://news.nationwide.com/homeowners-putting-off-home-upkeep-risking-damage/) - 65% of respondents who had something go wrong said they could have prevented it with proactive maintenance
---
### 2. "46% of homeowners spent $5,000+ on surprise repairs"
**Source:** Hippo Insurance / Bluefield Group Report (2024)
- [Average Homeowner Spent Over $5000 On Repairs In 2024](https://www.bluefieldgroup.com/blog/average-homeowner-spent-over-5000-on-repairs-in-2024/)
**Key Findings:**
- Almost half (46%) of surveyed homeowners reported spending more than $5,000 on unexpected home repairs in 2024
- This was a 10% increase from 2023 (36%)
- 83% of homeowners faced unexpected issues in 2024—nearly double the 46% reported in 2023
---
### 3. "47% of homebuyers lack emergency funds for repairs"
**Source:** Self Storage / Moving.com Survey
- [SURVEY: Nearly Half of Homebuyers Lack Emergency Savings for Home Costs](https://moving.selfstorage.com/cost-of-owning-a-home/)
**Key Findings:**
- 47% of American homebuyers lack an emergency fund to cover unexpected homeownership costs
- Only 21% of Americans feel very prepared for unexpected homeownership costs
- Over 1 in 3 panic when a home maintenance issue occurs due to lack of funds
---
### 4. "56% say sharing chores is key to a happy home"
**Source:** Pew Research Center (Religious Landscape Study)
- [Sharing chores a key to good marriage, say majority of married adults](https://www.pewresearch.org/short-reads/2016/11/30/sharing-chores-a-key-to-good-marriage-say-majority-of-married-adults/)
**Key Finding:**
- More than half of married U.S. adults (56%) say sharing household chores is "very important" to a successful marriage
---
## Additional Statistics for Reference
These statistics were found during research and may be useful for future marketing materials:
### HVAC & Filter Maintenance
- **82%** of Americans fail to change their air filter monthly ([The Zebra Survey](https://www.thezebra.com/resources/research/air-filter-survey/))
- **29%** of Americans never change their air filter at all
- **85%** of HVAC repairs are related to lack of proper maintenance ([Design Comfort](https://www.designcomfortco.com/blog/hvac-services/studies-show-hvac-maintenance-pays-for-itself/))
- HVAC replacement costs average $5,000-$10,000+ vs $200-400/year for maintenance
- Homeowners can save **15%** on energy bills by keeping up with filter replacement (U.S. Department of Energy)
### Cost of Neglect Examples
- Roof: $300-800/year maintenance vs $15,000-40,000 replacement
- Water damage: Average repair cost of $2,386-$3,342
- Foundation repairs: Average $8,000-15,000
- Emergency repairs average **$1,200+** vs ~$100 for preventive maintenance
### Homeowner Behavior
- **60%** of homeowners delay routine maintenance due to cost ([FinanceBuzz](https://financebuzz.com/homeowner-maintenance-repairs-survey))
- **59%** have put off necessary repairs due to cost ([AHS](https://www.ahs.com/home-matters/homebuyer-hub-resources-and-guides/home-maintenance-issues-statistics/))
- **41%** have paid for major repairs they believe could have been avoided with better maintenance ([Nationwide](https://news.nationwide.com/homeowners-putting-off-home-upkeep-risking-damage/))
### Annual Spending Data
- Average homeowner spends **$3,018/year** on maintenance (Angi Survey)
- Average homeowner spends **$2,321/year** on emergency repairs (Angi)
- Zillow/Thumbtack estimate **$9,390/year** on total home-related expenses
- Recommended savings: **1-4%** of home value annually for maintenance
### Digital Organization Benefits
- Families using digital organization tools report **37% more** completed responsibilities ([Journal of Family Psychology, 2023](https://www.familydaily.app/blog/digital-chore-chart-systems))
- **42% decrease** in household-related conflicts with digital tools
---
## Notes
- Statistics gathered December 2024
- All percentages rounded for presentation
- Sources should be periodically reviewed for updates
- Consider adding app-specific statistics as user data becomes available

View File

@@ -1,191 +0,0 @@
# TODO Audit - Incomplete Functionality
This document tracks all incomplete functionality, TODOs, and missing features across the iOS and Android/Kotlin codebases.
**Last Updated:** December 3, 2024
---
## iOS (SwiftUI)
### 1. Push Notification Navigation
**File:** `iosApp/iosApp/PushNotifications/PushNotificationManager.swift`
Three TODO items related to deep-linking from push notifications:
```swift
// TODO: Navigate to specific residence
// TODO: Navigate to specific task
// TODO: Navigate to specific contractor
```
**Status:** Push notifications are received but tapping them doesn't navigate to the relevant screen.
**Priority:** Medium - Improves user experience when responding to notifications.
---
### 2. File/Document Download
**File:** `iosApp/iosApp/Documents/DocumentDetailView.swift`
```swift
// TODO: Implement file download functionality
```
**Status:** Document viewing is partially implemented, but users cannot download/save documents to their device.
**Priority:** Medium - Documents can be viewed but not saved locally.
---
### 3. Subscription Upgrade Flow
**File:** `iosApp/iosApp/Subscription/FeatureComparisonView.swift`
```swift
// TODO: Implement upgrade functionality
```
**Status:** Feature comparison UI exists but the actual upgrade/purchase flow is not connected.
**Priority:** High - Required for monetization.
---
### 4. Widget App Groups
**File:** `iosApp/TaskWidgetExample.swift`
```swift
// TODO: Implement App Groups or shared container for widget data access.
```
**Status:** Widget shell exists but cannot access shared app data. Widgets always show empty state.
**Priority:** Low - Widgets are a nice-to-have feature.
---
## Android/Kotlin (Compose Multiplatform)
### 1. Document Download
**File:** `composeApp/src/commonMain/kotlin/com/example/honeydue/ui/screens/DocumentDetailScreen.kt`
```kotlin
// TODO: Download functionality
```
**Status:** Same as iOS - documents can be viewed but not downloaded.
**Priority:** Medium
---
### 2. Subscription Navigation from Residences
**File:** `composeApp/src/commonMain/kotlin/com/example/honeydue/ui/screens/ResidencesScreen.kt`
```kotlin
// TODO: Navigate to subscription/upgrade screen
```
**Status:** When user hits residence limit, there's no navigation to the upgrade screen.
**Priority:** High - Blocks user from upgrading when hitting limits.
---
### 3. Subscription Navigation from Residence Detail
**File:** `composeApp/src/commonMain/kotlin/com/example/honeydue/ui/screens/ResidenceDetailScreen.kt`
```kotlin
// TODO: Navigate to subscription screen
```
**Status:** Same issue - hitting task limit doesn't navigate to upgrade.
**Priority:** High
---
### 4. Profile Update Disabled
**File:** `composeApp/src/commonMain/kotlin/com/example/honeydue/ui/screens/ProfileScreen.kt`
```kotlin
// Update profile button is disabled/not implemented
```
**Status:** Profile editing UI exists but the save/update functionality may be incomplete.
**Priority:** Medium - Users expect to be able to edit their profile.
---
### 5. Contractor Favorite Toggle
**File:** `composeApp/src/commonMain/kotlin/com/example/honeydue/ui/screens/ResidenceDetailScreen.kt`
```kotlin
// Contractor favorite toggle not fully implemented
```
**Status:** Favorite toggle button exists on contractor cards but may not persist correctly.
**Priority:** Low
---
### 6. Platform-Specific Image Pickers
**Files:**
- `composeApp/src/jvmMain/kotlin/ImagePicker.jvm.kt`
- `composeApp/src/wasmJsMain/kotlin/ImagePicker.wasmJs.kt`
- `composeApp/src/jsMain/kotlin/ImagePicker.js.kt`
```kotlin
// TODO: Implement for Desktop
// TODO: Implement for WASM
// TODO: Implement for JS
```
**Status:** Image picker only works on mobile (Android/iOS). Desktop and web targets show placeholder implementations.
**Priority:** Low - Mobile is primary target.
---
## Summary by Priority
### High Priority
1. Subscription navigation from ResidencesScreen (Android)
2. Subscription navigation from ResidenceDetailScreen (Android)
3. Subscription upgrade flow (iOS)
### Medium Priority
4. Push notification navigation (iOS)
5. Document download (iOS & Android)
6. Profile update functionality (Android)
### Low Priority
7. Contractor favorite toggle (Android)
8. Widget App Groups (iOS)
9. Platform-specific image pickers (Desktop/Web)
---
## Recommendations
1. **Subscription Flow (High):** Both platforms need proper navigation to upgrade screens when users hit feature limits. This is critical for monetization.
2. **Push Notification Deep Links (Medium):** iOS push notification taps should navigate to the relevant residence/task/contractor detail screen.
3. **Document Download (Medium):** Implement share sheet / file saving for both platforms.
4. **Profile Update (Medium):** Verify the profile update API call is connected and working.
5. **Low Priority Items:** Widget, desktop/web image pickers, and contractor favorites can be addressed in future iterations.

View File

@@ -1,125 +0,0 @@
## Weekly Tasks (10)
| # | Task | Category | Description |
|---|------|----------|-------------|
| 3 | Wipe kitchen counters | Cleaning | Clean countertops and stovetop after cooking |
| 5 | Take out trash | Cleaning | Empty full trash cans to prevent odors and pests |
| 6 | Vacuum floors | Cleaning | Vacuum all carpets and rugs, especially high-traffic areas |
| 7 | Mop hard floors | Cleaning | Mop tile, hardwood, and laminate floors |
| 8 | Clean bathrooms | Cleaning | Scrub toilets, sinks, showers, and mirrors |
| 9 | Change bed linens | Cleaning | Wash and replace sheets, pillowcases, and mattress covers |
| 10 | Do laundry | Cleaning | Wash, dry, fold, and put away clothes |
| 11 | Clean kitchen appliances | Cleaning | Wipe down microwave, dishwasher exterior, coffee maker |
| 12 | Dust surfaces | Cleaning | Dust furniture, shelves, and decorations |
| 13 | Clean out refrigerator | Cleaning | Discard expired food and wipe down shelves |
| 14 | Water indoor plants | Landscaping | Check soil moisture and water as needed |
| 15 | Check/charge security cameras | Safety | Ensure wireless cameras are functioning and charged |
Check and/or replace water heater anode rod
Test interior water shutoffs
Test gas shutoffs
Test water meter shutoff
Check water meter for leaks
Run drain cleaner
Clean vacuum
Clean microwave
Clean and reverse ceiling fans (fall/spring)
Clean floor registers
Clean toaster
Mop floors 1/2
Clean bathroom exhaust fans
Clean garbage disposal
Flush HVAC drain lines
Test smoke and carbon monoxide detectors
Clean return vents
Test water heater pressure relief valve
Clean ovens
Clean fridge compressor coils
Clean dishwasher food trap
Mop floors 2/2
Check fire extinguishers
Replace water filters
Clear HVAC drain lines
Check water meter for leaks
Clean HVAC compressor coils
Test water sensors
Schedule chimney cleaning
Test GFCIs
Schedule HVAC inspection and service
Replace fridge hose
Replace smoke and carbon monoxide detectors
Replace laundry hoses
---
## Monthly Tasks (10)
| # | Task | Category | Description |
|---|------|----------|-------------|
| 16 | Change HVAC filters | HVAC | Replace air conditioning/furnace filters for efficiency |
| 17 | Test smoke detectors | Safety | Press test button on all smoke and CO detectors |
| 18 | Clean garbage disposal | Appliances | Run ice cubes and lemon peels to clean and deodorize |
| 19 | Inspect for leaks | Plumbing | Check under sinks and around toilets for water damage |
| 20 | Clean vent hood filters | Appliances | Soak and scrub range hood filters to remove grease |
| 21 | Vacuum under furniture | Cleaning | Move furniture to vacuum underneath, especially beds |
| 22 | Clean inside trash cans | Cleaning | Wash and disinfect garbage and recycling bins |
| 23 | Inspect caulking | Plumbing | Check bathroom and kitchen caulk for cracks or mold |
| 24 | Weed garden beds | Landscaping | Remove weeds and prune plants as needed |
| 25 | Check tire pressure | Vehicle | Inspect vehicle tires and refill as needed |
---
## Quarterly Tasks (8)
| # | Task | Category | Description |
|---|------|----------|-------------|
| 26 | Deep clean oven | Appliances | Clean inside oven; remove baked-on grease and spills |
| 27 | Clean refrigerator coils | Appliances | Vacuum dust from condenser coils for efficiency |
| 28 | Test GFCI outlets | Electrical | Press test/reset buttons on bathroom and kitchen outlets |
| 29 | Flush water heater | Plumbing | Drain sediment from bottom of tank |
| 30 | Clean dishwasher | Appliances | Run empty cycle with cleaner; clean filter and door seals |
| 31 | Inspect fire extinguishers | Safety | Check pressure gauge and ensure accessibility |
| 32 | Clean window tracks | Cleaning | Remove dirt and debris from window and door tracks |
| 33 | Pest control treatment | Safety | Inspect for signs of pests; treat or call professional |
---
## Semi-Annual Tasks (7)
| # | Task | Category | Description |
|---|------|----------|-------------|
| 34 | Clean gutters | Exterior | Remove leaves and debris; check for proper drainage |
| 35 | HVAC professional service | HVAC | Have system inspected before heating/cooling seasons |
| 36 | Clean dryer vent | Appliances | Remove lint buildup to prevent fires (15,500 fires/year) |
| 37 | Wash windows | Exterior | Clean interior and exterior glass and screens |
| 38 | Inspect roof | Exterior | Look for missing shingles, damage, or debris |
| 39 | Deep clean carpets | Cleaning | Professional carpet cleaning or DIY steam clean |
| 40 | Replace batteries | Safety | Replace smoke/CO detector batteries (if not hardwired) |
---
## Annual Tasks (10)
| # | Task | Category | Description |
|---|------|----------|-------------|
| 41 | Chimney/fireplace inspection | Safety | Professional inspection before first use of season |
| 42 | Septic tank inspection | Plumbing | Have septic system inspected and pumped if needed |
| 43 | Termite inspection | Safety | Professional inspection for wood-destroying insects |
| 44 | Service garage door | Exterior | Lubricate springs, hinges, and rollers |
| 45 | Inspect weather stripping | Exterior | Check doors and windows; replace worn seals |
| 46 | Winterize outdoor faucets | Plumbing | Shut off water supply and drain lines before freeze |
| 47 | Pressure wash exterior | Exterior | Clean siding, driveway, sidewalks, and deck |
| 48 | Touch up exterior paint | Exterior | Address peeling or cracking paint to prevent moisture damage |
| 49 | Service sprinkler system | Landscaping | Inspect heads, adjust coverage, winterize if needed |
| 50 | Replace washing machine hoses | Appliances | Replace rubber hoses to prevent flooding |
## Sources
- [Care.com - Ultimate Household Chore List](https://www.care.com/c/ultimate-household-chore-list/)
- [Bungalow - Complete Household Chores List](https://bungalow.com/articles/the-complete-household-chores-list)
- [AHIT - Home Maintenance Checklist](https://www.ahit.com/home-inspection-career-guide/home-maintenance-checklist/)
- [Homebuyer.com - Home Maintenance Checklist](https://homebuyer.com/learn/home-maintenance-checklist)
- [Frontdoor - Home Maintenance Checklist](https://www.frontdoor.com/blog/handyman-tips/ultimate-home-maintenance-checklist)
- [Travelers Insurance - Monthly Home Maintenance](https://www.travelers.com/resources/home/maintenance/home-maintenance-checklist-10-easy-things-to-do-monthly)
- [American Family Insurance - Home Maintenance](https://www.amfam.com/resources/articles/at-home/home-maintenance-checklist)

View File

@@ -1,577 +0,0 @@
# Hardening Audit Report
## Audit Sources
- 11 mapper agents (100% file coverage)
- 17 specialized domain auditors (parallel)
- 1 cross-cutting deep audit (parallel)
- Total source files: 161
---
## CRITICAL — Will crash or lose data (14 findings)
**WidgetDataManager.swift:248** | Missing closing brace nests all remaining class members inside `clearPendingState` function
- What: The `clearPendingState()` method is missing its closing `}`. All subsequent members (`hasPendingActions`, `WidgetTask`, `TaskMetrics`, `calculateMetrics`, `saveTasks`, `loadTasks`, etc.) are nested inside the function scope, making them inaccessible externally.
- Impact: Build failure. External callers (`DataManagerObservable`, `iOSApp`, `BackgroundTaskManager`, `WidgetActionProcessor`, etc.) cannot access these members.
- Source: Deep Audit (cross-cutting)
**StoreKitManager.swift:91-94** | Transaction finished even when backend verification fails
- What: After StoreKit transaction verification, `verifyTransactionWithBackend(transaction)` is called at line 91, then `transaction.finish()` is called unconditionally at line 94. Backend errors are logged but not propagated.
- Impact: User charged by Apple but backend never records the purchase. Finished transactions cannot be re-verified via `Transaction.currentEntitlements`. User stuck on free tier despite paying. Same pattern in `listenForTransactions()` at lines 234-256.
- Source: Deep Audit (cross-cutting), IAP Auditor
**AppleSignInManager.swift:5** | `ObservableObject` without `@MainActor` publishes from delegate callbacks on background threads
- What: `ASAuthorizationControllerDelegate` callbacks can deliver on background threads. Inside these callbacks, `isProcessing = false` and `self.error = ...` mutate `@Published` properties off the main thread.
- Impact: Data races and potential SwiftUI rendering crashes.
- Source: Concurrency Auditor
**AppleSignInManager.swift:113** | `presentationAnchor(for:)` accesses UIKit APIs without `@MainActor`
- What: Reads `UIApplication.shared.connectedScenes` and `scene.windows` from a non-`@MainActor` method.
- Impact: Accessing UIKit from a background thread is undefined behavior and frequently crashes.
- Source: Concurrency Auditor
**StoreKitManager.swift:7** | `ObservableObject` without `@MainActor`, `@Published` mutations from `Task.detached`
- What: `StoreKitManager` has `@Published` properties but no `@MainActor`. `listenForTransactions()` creates a `Task.detached` that accesses `self` without actor isolation.
- Impact: `@Published` mutations from detached task run off main actor. Swift 6 compiler error. Potential crashes from concurrent access.
- Source: Concurrency Auditor
**StoreKitManager.swift:234** | `Task.detached` captures `self` strongly without `[weak self]`
- What: `listenForTransactions()` creates `Task.detached { ... }` capturing `self` strongly. Called `checkVerified` on `self` without actor isolation.
- Impact: Swift 6 strict mode error: "Sending 'self' risks causing data races."
- Source: Concurrency Auditor
**iOSApp.swift:294** | Deep link reset-password token extracted but never delivered to any view
- What: `handleDeepLink` stores parsed reset token in `@State private var deepLinkResetToken`, but `RootView()` is constructed with no arguments. `LoginView` accepts `resetToken: Binding<String?>` but the binding is never wired.
- Impact: `honeydue://reset-password?token=xxx` deep links are silently discarded. Password reset emails don't work.
- Source: Navigation Auditor
**Info.plist** | Missing Privacy Manifest (`PrivacyInfo.xcprivacy`)
- What: No `PrivacyInfo.xcprivacy` file found. App uses `UserDefaults`, analytics (PostHog), and device identifiers — all require declared API reasons since iOS 17.
- Impact: App Store rejection starting Spring 2024 enforcement. Required for `NSPrivacyAccessedAPIType` declarations.
- Source: Security/Privacy Scanner
**DoubleExtensions.swift:42** | Shared `NumberFormatter` mutated on every call — data race
- What: `toDecimalString(fractionDigits:)` and `toPercentage(fractionDigits:)` mutate `minimumFractionDigits`/`maximumFractionDigits` on shared `NumberFormatters.shared` instances. No lock or actor protection.
- Impact: Concurrent calls from `LazyVStack` rendering will race on formatter property writes. Non-deterministic output; rare but real crash.
- Source: SwiftUI Architecture Auditor, SwiftUI Performance Analyzer
**Info.plist:61** | `fetch` background mode declared but never implemented
- What: `UIBackgroundModes` includes `"fetch"` but there is no `application(_:performFetchWithCompletionHandler:)` implementation. App uses `BGAppRefreshTask` instead.
- Impact: iOS penalizes apps with unused background modes. System wakes app for fetch cycles with no useful work. Risk of App Store rejection.
- Source: Energy Auditor
**WidgetDataManager.swift:43,57,64,79,110,122** | `UserDefaults.synchronize()` called on every small write — 6 forced disk syncs
- What: `synchronize()` is called after each individual write to shared UserDefaults (auth token save/clear, API URL, subscription status, dirty flag). Forces immediate disk flush instead of batching.
- Impact: Each call triggers synchronous disk write, waking storage controller. 1-3% additional battery drain per active session hour.
- Source: Energy Auditor
**DataManagerObservable.swift:178** | Widget task file written on every DataManager tasks emission
- What: `WidgetDataManager.shared.saveTasks(from: tasks)` called every time `allTasks` emits. Writes JSON file, encodes all tasks with `.prettyPrinted`, calls `WidgetCenter.shared.reloadAllTimelines()` — all synchronously.
- Impact: Every API call touching tasks triggers JSON encoding, atomic file write, and widget timeline reload. 3-8% additional battery drain during active use.
- Source: Energy Auditor
**DataManager.kt:367-368** | `tasksDueNextWeek` and `tasksDueNextMonth` set to the same value
- What: Both assigned `dueSoonCount` from `due_soon_tasks` column (30-day window). No separate 7-day calculation.
- Impact: Dashboard shows identical numbers for "Due This Week" and "Due Next Month." Weekly metric is useless.
- Source: Deep Audit (cross-cutting)
**ResidenceDetailView.swift:426** | `APILayer` called directly from a View — architecture boundary violation
- What: `deleteResidence()` and `loadResidenceContractors()` are functions on the View struct calling `APILayer.shared` directly, managing `@State` loading/error booleans.
- Impact: Business logic untestable, cannot be mocked, violates declared architecture. Same issue in `ManageUsersView.swift:109`.
- Source: SwiftUI Architecture Auditor
---
## BUG — Incorrect behavior (8 findings)
**WidgetDataManager.swift:247** | `hasPendingActions` var declared inside `clearPendingState` method body
- What: Due to missing closing brace, `var hasPendingActions: Bool` is a local variable inside the method, not an instance computed property.
- Impact: `hasPendingActions` inaccessible from `WidgetActionProcessor`. Build failure.
- Source: Concurrency Auditor
**AnalyticsManager.swift:19** | Identical PostHog API key in both DEBUG and RELEASE builds
- What: `#if DEBUG / #else` block assigns same `phc_oeZddTz7Iw0NxDXFeCycS7TG9YRVtv7WP2DjOvFPUeQ` key in both branches. Has `// TODO: (SE2)` comment.
- Impact: Debug/QA events pollute production analytics, corrupting funnel metrics.
- Source: Security/Privacy Scanner, SwiftUI Architecture Auditor
**AuthViewModel.kt:87-88** | `register()` double-writes auth token and user to DataManager
- What: Calls `DataManager.setAuthToken()` and `DataManager.setCurrentUser()` which APILayer.register() already calls internally.
- Impact: Double StateFlow emissions, duplicate disk persistence, unnecessary UI re-renders.
- Source: Deep Audit (cross-cutting)
**LoginViewModel.swift:99-105** | `login()` calls `initializeLookups()` after `APILayer.login()` which already calls it internally
- What: Double initialization of all lookup data on every login.
- Impact: Second round of ETag-based fetches, delays post-login navigation on slow connections.
- Source: Deep Audit (cross-cutting)
**OnboardingState.swift:16** | Dual source of truth for `hasCompletedOnboarding` between `@AppStorage` and Kotlin `DataManager`
- What: `@AppStorage("hasCompletedOnboarding")` in Swift and `_hasCompletedOnboarding` StateFlow in Kotlin are never synchronized. `completeOnboarding()` sets @AppStorage but NOT Kotlin DataManager.
- Impact: Inconsistent onboarding state between Swift and Kotlin layers. Could show onboarding again after certain logout/clear flows.
- Source: Deep Audit (cross-cutting)
**ResidencesListView.swift:149** | Deprecated `NavigationLink(isActive:)` used for push-notification navigation
- What: Hidden `NavigationLink(isActive:destination:label:)` in `.background` modifier for programmatic navigation. Deprecated since iOS 16.
- Impact: Unreliable with `NavigationStack`. May silently fail or double-push. Same pattern in `DocumentsWarrantiesView.swift:210` and `DocumentDetailView.swift:47`.
- Source: Navigation Auditor
**DateExtensions.swift:97-133** | DateFormatter created per call in extension methods without locale pinning
- What: Six `DateFormatter()` instances created in extension methods without setting `locale` to `Locale(identifier: "en_US_POSIX")` for fixed-format dates.
- Impact: Formatting varies by user locale. API date strings may be incorrectly formatted in non-English locales.
- Source: Codable Auditor
**AllTasksView.swift:94** | `loadAllTasks(forceRefresh: true)` called from view after sheet dismissal
- What: Calls refresh methods in `.onChange(of: showAddTask)` and `.onChange(of: showEditTask)` when sheets close.
- Impact: Violates CLAUDE.md architecture rule: "iOS code MUST ONLY call mutation methods on ViewModels, NOT refresh methods after mutations."
- Source: SwiftUI Architecture Auditor
---
## SILENT FAILURE — Error swallowed or ignored (6 findings)
**StoreKitManager.swift:259-295** | `verifyTransactionWithBackend` swallows all errors
- What: Backend verification errors are printed but never thrown or returned. Caller has no way to know verification failed.
- Impact: Transactions finished without backend acknowledgment. Revenue lost silently.
- Source: IAP Auditor, Deep Audit
**StateFlowObserver.swift:24** | `Task` returned without storing — caller may not retain, causing premature cancellation
- What: `observe()` creates and returns a `Task`, but `observeWithState` and `observeWithCompletion` (lines 69, 103) discard the returned task. `@discardableResult` suppresses the warning.
- Impact: Observation stops immediately. `onSuccess`/`onError` callbacks never fire.
- Source: Concurrency Auditor
**Codable patterns across codebase** | `try?` used extensively to swallow JSON errors
- What: Multiple locations use `try?` for JSON decoding/encoding without error logging.
- Impact: Malformed data silently produces nil instead of surfacing the issue.
- Source: Codable Auditor
**DocumentFormState.swift:89** | DateFormatter without locale for API date formatting
- What: DateFormatter created with `dateFormat = "yyyy-MM-dd"` but no `locale` set.
- Impact: On devices with non-Gregorian calendars, date string may not match expected API format.
- Source: Codable Auditor
**GoogleSignInManager.swift** | Singleton closure leak
- What: `GoogleSignInManager` singleton captures `self` strongly in completion handlers.
- Impact: Memory leak in singleton (benign for singleton, bad pattern for reuse).
- Source: Memory Auditor
**AuthenticatedImage.swift:86** | Static `NSCache` unbounded across all instances
- What: `NSCache<NSString, UIImage>` has no `countLimit` or `totalCostLimit`. Not cleared on logout.
- Impact: Excessive memory pressure. Stale images from previous user session may display briefly.
- Source: Networking Auditor, Memory Auditor
---
## RACE CONDITION — Concurrency issue (9 findings)
**SubscriptionCacheWrapper.swift:10** | `ObservableObject` without `@MainActor`
- What: Four `@Published` properties, no `@MainActor`. Uses `DispatchQueue.main.async` for writes instead of actor isolation.
- Impact: Swift 6 isolation checker loses thread context. Refactoring that removes `async` breaks thread safety.
- Source: Concurrency Auditor
**ThemeManager.swift:95** | `ObservableObject` without `@MainActor`
- What: `@Published var currentTheme`, no `@MainActor`. `setTheme(_:)` calls `withAnimation` which must be on main actor.
- Impact: `withAnimation` silently no-ops off main actor. Swift 6 data race.
- Source: Concurrency Auditor
**OnboardingState.swift:12** | `ObservableObject` without `@MainActor`
- What: Multiple `@Published` properties, singleton, no `@MainActor`. `nextStep()` and `completeOnboarding()` mutate state without actor guarantee.
- Impact: Swift 6 strict mode error from any `Task {}` call to mutation methods.
- Source: Concurrency Auditor
**WidgetDataManager.swift:7** | `fileQueue.sync` blocks main thread from `@MainActor` call sites
- What: `saveTasks`, `loadTasks`, `removeAction`, `clearPendingState` use `fileQueue.sync` blocking the calling thread. Called from `@MainActor` code in `DataManagerObservable`.
- Impact: Blocks main thread during file I/O, causing frame drops and potential watchdog terminations.
- Source: Concurrency Auditor
**DataManagerObservable.swift:96** | All 27 observation `Task` blocks capture `self` strongly
- What: `startObserving()` creates 27 `Task { for await ... }` blocks, none using `[weak self]`.
- Impact: Pattern prevents deallocation. Swift 6 Sendable checker may flag captures.
- Source: Concurrency Auditor
**PushNotificationManager.swift:72** | Multiple `Task {}` without `[weak self]` inside `@MainActor` class
- What: Lines 72, 90, 102, 179, 196, 232, 317, 349, 369, 391 — all capture `self` strongly.
- Impact: Pattern violation under Swift 6 strict concurrency.
- Source: Concurrency Auditor
**AppDelegate.swift:6** | `AppDelegate` missing `@MainActor` but wraps `@MainActor` singleton
- What: Conforms to `UIApplicationDelegate` and `UNUserNotificationCenterDelegate` without `@MainActor`. Calls `PushNotificationManager.shared` via `Task { @MainActor in }` correctly, but class-level isolation missing.
- Impact: Swift 6 compiler error for any future direct property access.
- Source: Concurrency Auditor
**SubscriptionCache.swift:157** | `DispatchQueue.main.async` used instead of `@MainActor` isolation for `@Published` writes
- What: Pre-Swift-concurrency pattern. Multiple methods dispatch to main queue manually.
- Impact: Mixing `DispatchQueue.main.async` with Swift concurrency actors loses isolation tracking.
- Source: Concurrency Auditor
**CompleteTaskView.swift:336** | `Task { for await ... }` started in action handler without cancellation management
- What: Task observing Kotlin StateFlow started in view method with no cancellation token. Sheet dismissal doesn't cancel it.
- Impact: `onComplete` callback fires after view gone, triggering unwanted state changes.
- Source: SwiftUI Architecture Auditor
---
## LOGIC ERROR — Code doesn't match intent (4 findings)
**SubscriptionCache.swift:114** | `objectWillChange` observation fires on ALL DataManagerObservable changes
- What: Subscribes to `DataManagerObservable.shared.objectWillChange` (25+ @Published properties). Every task update, residence change, lookup update triggers `syncFromDataManager()`.
- Impact: Unnecessary Kotlin StateFlow reads on every unrelated data change. Should observe `$subscription` directly.
- Source: SwiftUI Architecture Auditor, Deep Audit
**OnboardingState.swift:12** | Mixes `@AppStorage` with `@Published` — potential update-notification gap
- What: Uses both `@AppStorage` (for persistence) and `@Published` (for observation). `userIntent` computed property mutates `@AppStorage` and calls `objectWillChange.send()` manually.
- Impact: Duplicate or missed notifications. SwiftUI reactivity bugs in onboarding flow.
- Source: SwiftUI Architecture Auditor
**ResidencesListView.swift:133** | Duplicate `onChange(of: authManager.isAuthenticated)` observers
- What: Two observers for `authManager.isAuthenticated` in `ResidencesListView` plus one in `MainTabView`. All fire on the same state change.
- Impact: `loadMyResidences()` and `loadTasks()` called multiple times from different observers. Redundant network requests.
- Source: Navigation Auditor
**ApiConfig.kt:25** | DEV environment URL doesn't match CLAUDE.md documentation
- What: Code uses `https://honeyDue.treytartt.com/api`. CLAUDE.md documents `https://honeyDue.treytartt.com/api`.
- Impact: Documentation misleads developers.
- Source: Deep Audit (cross-cutting)
---
## PERFORMANCE — Unnecessary cost (19 findings)
**DataManagerObservable.swift:18** | Monolithic `ObservableObject` with 30+ `@Published` properties
- What: Single class with 30+ properties covering auth, residences, tasks, documents, contractors, subscriptions, lookups. Any property change invalidates ALL subscribed views.
- Impact: O(views x changes) invalidation. Loading contractors re-renders all views observing DataManager.
- Source: SwiftUI Performance Analyzer, Swift Performance Analyzer
**PropertyHeaderCard.swift:145** | `NumberFormatter()` created on every view body evaluation
- What: `formatNumber()` creates a new `NumberFormatter()` each call. Called from `var body` inside a `ForEach` on residence list.
- Impact: ~1-2ms per allocation. Stutter on scroll frames. Shared formatter already exists in `NumberFormatters.shared`.
- Source: SwiftUI Performance Analyzer
**DocumentsWarrantiesView.swift:26** | `filter()` on documents array called as computed property in view body
- What: `warranties` and `documents` computed properties filter entire array on every state change.
- Impact: Fires on every keystroke during search. Same issue in `WarrantiesTabContent.swift:10` and `DocumentsTabContent.swift:13`.
- Source: SwiftUI Performance Analyzer
**ContractorsListView.swift:25** | `filter()` with nested `.contains()` on specialties in view body
- What: O(n*m) scan on every view update — each contractor's specialties checked on every render.
- Impact: Fires on every search character, favorite toggle, any @Published change.
- Source: SwiftUI Performance Analyzer
**DataManagerObservable.swift:573** | O(n) linear scan for per-residence task metrics
- What: `activeTaskCount(for:)` and `taskMetrics(for:)` iterate all task columns per residence per render.
- Impact: O(tasks x residences) computation on every render pass.
- Source: SwiftUI Architecture Auditor
**OrganicDesign.swift:104-125** | `GrainTexture` renders random noise via `Canvas` on every draw pass — used in 25+ files
- What: `Canvas` closure calls `CGFloat.random` for every pixel subdivision on every render pass. No caching.
- Impact: ~390 draw calls per 390x200pt card per redraw. During animation: 60+ times/sec. 3-7% GPU drain.
- Source: Energy Auditor
**LoadingOverlay.swift:127** | Shimmer animation runs `repeatForever` with no stop mechanism
- What: `withAnimation(.linear(duration: 1.5).repeatForever(...))` with no `.onDisappear` to stop.
- Impact: GPU compositing continues even when not visible if view remains in hierarchy.
- Source: Energy Auditor
**OrganicDesign.swift:405-415** | `FloatingLeaf` runs `repeatForever` animation with no stop — used in 3+ empty-state views
- What: 4-second `repeatForever` animation driving rotation and offset. No `onDisappear` to stop.
- Impact: Animations remain active in navigation stack hierarchy. 5-10% battery drain per hour.
- Source: Energy Auditor
**ResidenceCard.swift:197-205** | `PulseRing` runs infinite animation per card with no stop
- What: 1.5-second `repeatForever` per residence card with overdue tasks. No `onDisappear`.
- Impact: 5 residences = 5 concurrent infinite animations. 5-12% GPU drain per hour.
- Source: Energy Auditor
**Onboarding views (8 screens)** | `repeatForever` animations stack without cleanup
- What: Each onboarding screen starts independent `repeatForever` animations. No `onDisappear` to stop. By last step, 10+ concurrent animations active.
- Impact: 10-20% battery drain during onboarding flow.
- Source: Energy Auditor
**WidgetDataManager.swift:407** | `reloadAllTimelines()` called unconditionally inside `saveTasks()`
- What: Also called from `DataManagerObservable`, `BackgroundTaskManager`, and `iOSApp.swift` on background. Multiple back-to-back reloads.
- Impact: 3-6% battery drain per active hour from unnecessary widget renders.
- Source: Energy Auditor
**ResidencesListView.swift:296-318** | Two concurrent `repeatForever` animations in empty-state with no stop
- What: Scale on glow circle (3s) + y-offset on house icon (2s). Remain in navigation hierarchy.
- Impact: Same pattern in `AllTasksView.swift:331-349` (2 animations), `ContractorsListView.swift:432-436` (1 animation).
- Source: Energy Auditor
**Info.plist** | `CADisableMinimumFrameDurationOnPhone = true` enables 120fps without selective opt-in
- What: All decorative `repeatForever` animations run at 120fps on ProMotion devices.
- Impact: Doubles GPU compositing work for purely decorative content. 5-10% additional drain.
- Source: Energy Auditor
**Kotlin byte-by-byte conversion** | Data crossing KMM bridge with unnecessary copies
- What: Kotlin StateFlow observations involve byte-by-byte conversion at the Swift-Kotlin boundary.
- Impact: O(n) copy overhead on every StateFlow emission for large data sets.
- Source: Swift Performance Analyzer
**UpgradePromptView.swift:10** | `parseContent()` string parsing called on every render
- What: `lines` computed property calls `parseContent(content)` (full string splitting/classification) on every render.
- Impact: String parsing ~10-50x more expensive than property access.
- Source: SwiftUI Performance Analyzer
**DynamicTaskCard.swift:142** | Three `.contains(where:)` calls in `menuContent` view body
- What: Each renders per task card in kanban columns. 10+ tasks = 30+ array scans per render.
- Impact: Measurable overhead in scrolling kanban view.
- Source: SwiftUI Performance Analyzer
**ContractorDetailView.swift:485** | `.first(where:)` on full residences array in view body
- What: Scans entire residences array on every render of contractor detail.
- Impact: Unnecessary O(n) on every layout pass.
- Source: SwiftUI Performance Analyzer
**AuthenticatedImage.swift:139** | `URLSession.shared` for image downloads with no cellular constraints
- What: No `allowsExpensiveNetworkAccess = false` or `isDiscretionary = true`.
- Impact: Keeps cellular modem powered up longer on poor connections. 3-8% additional drain.
- Source: Energy Auditor, Networking Auditor
**Ktor clients (all platforms)** | No `HttpTimeout` configured
- What: None of the five Ktor `createHttpClient()` implementations install `HttpTimeout`. Default is infinite.
- Impact: Stalled TCP connection hangs coroutine indefinitely with no error surfaced.
- Source: Networking Auditor
---
## ACCESSIBILITY — Usability barrier (24 findings)
**Multiple views** | Missing Dynamic Type support — fixed font sizes throughout
- What: Extensive use of `.font(.system(size: N))` with hardcoded sizes across all views (onboarding, residence cards, contractor cards, task cards, document views, subscription views).
- Impact: Text doesn't scale with user's accessibility settings. Violates WCAG 2.1 SC 1.4.4.
- Source: Accessibility Auditor
**Multiple views** | Missing VoiceOver labels on interactive elements
- What: Buttons using only SF Symbols without `.accessibilityLabel()`. Decorative images without `.accessibilityHidden(true)`.
- Impact: VoiceOver users hear "Button" or image filename instead of meaningful description.
- Source: Accessibility Auditor
**OrganicDesign.swift** | Animations not respecting `accessibilityReduceMotion`
- What: `repeatForever` animations (FloatingLeaf, PulseRing, shimmer, blob pulses) have no `@Environment(\.accessibilityReduceMotion)` check.
- Impact: Users with motion sensitivity experience nausea or discomfort.
- Source: Accessibility Auditor
**Multiple views** | Missing `.accessibilityElement(children: .combine)` on card views
- What: Card views with multiple text elements not combined for VoiceOver.
- Impact: VoiceOver navigates through each piece of text separately instead of reading the card as a unit.
- Source: Accessibility Auditor
---
## SECURITY — Vulnerability or exposure (10 findings)
**Missing PrivacyInfo.xcprivacy** | No Privacy Manifest
- What: Required since iOS 17 for UserDefaults, analytics, device identifiers.
- Impact: App Store rejection.
- Source: Security/Privacy Scanner
**AnalyticsManager.swift:19** | Same PostHog API key in DEBUG and RELEASE
- What: Both branches use identical key. Debug events pollute production.
- Impact: Corrupted analytics data.
- Source: Security/Privacy Scanner
**ApiClient.js.kt:35, ApiClient.wasmJs.kt:35, ApiClient.jvm.kt:35** | `LogLevel.ALL` logs all HTTP traffic in production
- What: Auth tokens and PII in JSON bodies appear in browser console/logs. No DEBUG guard on JS, wasmJs, JVM targets.
- Impact: Auth token leakage in production environments.
- Source: Networking Auditor
**WidgetDataManager.swift** | Auth token stored in shared UserDefaults
- What: Auth token saved to shared App Group UserDefaults for widget access.
- Impact: Accessible to any app in the App Group. Should use Keychain shared access group.
- Source: Storage Auditor, Security/Privacy Scanner
**Multiple files** | `print()` statements with sensitive data in production code
- What: Debug prints containing user data, tokens, and state information throughout codebase.
- Impact: Sensitive data visible in device console logs.
- Source: Security/Privacy Scanner
---
## MODERNIZATION — Legacy pattern to update (33 findings)
**All ViewModels** | `ObservableObject` + `@Published` instead of `@Observable` macro
- What: All iOS ViewModels use legacy `ObservableObject` pattern. iOS 17+ supports `@Observable` with property-level observation.
- Impact: Whole-object invalidation instead of property-level tracking. Excessive re-renders.
- Source: Modernization Helper
**Multiple views** | `NavigationView` usage (deprecated since iOS 16)
- What: `ResidencesListView.swift:103` (ProfileTabView sheet), `AllTasksView.swift:461` (preview), `ContractorsListView.swift:466` (preview).
- Impact: Split-view behavior on iPad. Deprecated API.
- Source: Navigation Auditor, Modernization Helper
**3 files** | Deprecated `NavigationLink(isActive:)` pattern
- What: `ResidencesListView.swift:149`, `DocumentsWarrantiesView.swift:210`, `DocumentDetailView.swift:47` use hidden background `NavigationLink(isActive:)`.
- Impact: Incompatible with `NavigationStack`. Unreliable programmatic navigation.
- Source: Navigation Auditor
**MainTabView.swift:12** | No `NavigationPath` on any tab NavigationStack
- What: All four tab-root NavigationStacks declared without `path:` binding.
- Impact: No programmatic navigation, no state restoration, no deep linking support.
- Source: Navigation Auditor
**iOSApp.swift:1** | No `@SceneStorage` for navigation state
- What: No navigation state persistence anywhere.
- Impact: All navigation position lost on app termination.
- Source: Navigation Auditor
**MainTabView.swift:1** | No `.navigationDestination(for:)` registered
- What: Zero type-safe navigation contracts. All `NavigationLink` use inline destination construction.
- Impact: Cannot add deep links or programmatic navigation without touching every call site.
- Source: Navigation Auditor
**PasswordResetViewModel.swift:62** | `DispatchQueue.main.asyncAfter` instead of `Task.sleep`
- What: Hard-coded 1.5-second timers using GCD instead of Swift concurrency.
- Impact: No cancellation support. Closure fires after view dismissal.
- Source: Modernization Helper, SwiftUI Architecture Auditor
**AnalyticsManager.swift:3, ThemeManager.swift:1** | Non-View managers import SwiftUI
- What: Infrastructure classes coupled to UI framework.
- Impact: Cannot unit test without SwiftUI dependency chain.
- Source: SwiftUI Architecture Auditor
**FormStates (4 files)** | Non-View form state models import SwiftUI for UIImage
- What: `DocumentFormState`, `CompleteTaskFormState`, `ContractorFormState`, `ResidenceFormState` store `UIImage`/`[UIImage]`.
- Impact: Business validation logic coupled to UIKit, preventing unit testing.
- Source: SwiftUI Architecture Auditor
---
## DEAD CODE / UNREACHABLE (2 findings)
**ContentView.swift** | Deleted file tracked in git
- What: File deleted (shown in git status as `D`) but changes not committed.
- Impact: Unused file artifact.
- Source: Mapper
**StateFlowExtensions.swift** | Deleted file tracked in git
- What: File deleted but changes not committed.
- Impact: Unused file artifact.
- Source: Mapper
---
## FRAGILE — Works now but will break easily (6 findings)
**MainTabView.swift** | No coordinator/router pattern — navigation logic scattered across 5+ views
- What: Push notification routing in `PushNotificationManager`, consumed individually in `MainTabView`, `AllTasksView`, `ResidencesListView`, `DocumentsWarrantiesView`. Deep links in `iOSApp`. Auth navigation in `ResidencesListView`.
- Impact: Adding new deep link destination requires touching 3+ files.
- Source: Navigation Auditor
**APILayer.kt:97-100** | Split mutex unlock pattern
- What: `initializeLookups()` has manual unlock at line 98 (early return) and `finally` unlock at line 188. Correct but fragile.
- Impact: Easy to break during refactoring.
- Source: Deep Audit (cross-cutting)
**SubscriptionCache.swift:114-118** | Reads Kotlin DataManager directly instead of DataManagerObservable
- What: Bypasses established observation pattern (every other component uses DataManagerObservable's @Published properties).
- Impact: If Kotlin StateFlow emission timing changes, could read stale data.
- Source: Deep Audit (cross-cutting)
**iOSApp.swift:55-64** | `initializeLookups()` called 4 times: init, APILayer.login, LoginViewModel, foreground
- What: Quadruple initialization on cold start after login.
- Impact: Wasted bandwidth and delayed navigation.
- Source: Deep Audit (cross-cutting)
**ResidencesListView.swift:125** | `fullScreenCover` bound to `isAuthenticated.negated` — custom Binding negation
- What: Inline `Binding` negation with inverted setter (`set: { self.wrappedValue = !$0 }`).
- Impact: Fragile pattern. Dismissal fires unintended side effects.
- Source: Navigation Auditor
**NotificationPreferencesView.swift:385** | ViewModel calls APILayer directly, bypasses DataManager cache
- What: `NotificationPreferencesViewModelWrapper` calls `APILayer.shared` directly. Results never cached in DataManager.
- Impact: Always shows loading spinner. Inconsistent with architecture.
- Source: SwiftUI Architecture Auditor
---
## TESTING (23 findings)
**UI Tests** | 409 `sleep()` calls across UI test suites
- What: `Thread.sleep(forTimeInterval:)` used extensively for timing. Brittle and slow.
- Impact: Tests take longer than necessary. Flaky on slow CI.
- Source: Testing Auditor
**UI Tests** | Shared mutable state across test suites
- What: Test suites share state without proper isolation.
- Impact: Tests pass individually but fail when run together.
- Source: Testing Auditor
**Unit Tests** | Minimal test coverage — only 2 unit test suites
- What: Only `honeyDueTests.swift` (template) and `TaskMetricsTests.swift`. No ViewModel tests, no form validation tests, no integration tests.
- Impact: No regression safety net for business logic.
- Source: Testing Auditor
**Build** | Missing incremental compilation settings
- What: Build settings not optimized for incremental compilation.
- Impact: Slower build times during development.
- Source: Build Optimizer
**Build** | `alwaysOutOfDate` on Kotlin script build phase
- What: Kotlin framework build phase always runs, even when source hasn't changed.
- Impact: Unnecessary rebuild time on every build.
- Source: Build Optimizer
---
## Summary
### Summary by Category
| Category | Count |
|----------|-------|
| Critical | 14 |
| Bug | 8 |
| Silent Failure | 6 |
| Race Condition | 9 |
| Logic Error | 4 |
| Performance | 19 |
| Accessibility | 24 |
| Security | 10 |
| Modernization | 33 |
| Dead Code | 2 |
| Fragile | 6 |
| Testing | 23 |
| **Total** | **158** |
### Summary by Source
| Source | Findings |
|--------|----------|
| Concurrency Auditor | 21 |
| Memory Auditor | 8 |
| SwiftUI Performance | 15 |
| Swift Performance | 17 |
| SwiftUI Architecture | 18 |
| Security/Privacy | 10 |
| Accessibility | 24 |
| Energy | 17 |
| Storage | 5 |
| Networking | 7 |
| Codable | 19 |
| IAP | 12 |
| iCloud | 5 |
| Modernization | 33 |
| Navigation | 15 |
| Testing | 23 |
| Build Optimization | 8 |
| Deep Audit (cross-cutting) | 10 |
*Note: Some findings were reported by multiple auditors and deduplicated. Raw total across all auditors was ~267; after dedup: 158.*
### Top 10 Priorities
1. **CRITICAL: Fix `WidgetDataManager.swift:248` missing closing brace** — Build blocker. Every member after `clearPendingState` is nested inside the function.
2. **CRITICAL: Fix `StoreKitManager` transaction finish-before-verify** — Users charged by Apple but backend doesn't record. Revenue loss. Finish only after backend confirms.
3. **CRITICAL: Add `@MainActor` to `AppleSignInManager`, `StoreKitManager`, `SubscriptionCacheWrapper`, `ThemeManager`, `OnboardingState`** — All are ObservableObject with @Published mutations from non-main-actor contexts. Data races and potential crashes.
4. **CRITICAL: Wire deep link reset-password token to LoginView** — Password reset emails completely broken. `deepLinkResetToken` never reaches `RootView``LoginView`.
5. **CRITICAL: Add Privacy Manifest (`PrivacyInfo.xcprivacy`)** — App Store rejection risk. Required for UserDefaults, PostHog analytics, device identifiers.
6. **CRITICAL: Remove `UserDefaults.synchronize()` calls and debounce widget saves** — 6 forced disk syncs + JSON write on every task emission. Combined 5-10% battery drain.
7. **HIGH: Stop all `repeatForever` animations on `.onDisappear`** — 15+ infinite animations across onboarding, empty states, cards, shimmer, floating leaves. Combined 10-20% GPU drain.
8. **HIGH: Migrate to `NavigationStack(path:)` with `.navigationDestination(for:)`** — Replace all 3 deprecated `NavigationLink(isActive:)` patterns. Enable programmatic navigation and state restoration.
9. **HIGH: Fix `NumberFormatters.shared` thread safety** — Shared formatter mutated on every call from view body. Data race under concurrent rendering.
10. **HIGH: Split `DataManagerObservable` or migrate to `@Observable`** — 30+ @Published properties cause every view to re-render on any data change. Systemic performance issue.

View File

@@ -1,158 +0,0 @@
# Accessibility Identifiers - Duplicate Fix
## Issue
The original `AccessibilityIdentifiers.swift` file had duplicate `cancelButton` declarations within the same struct, causing a compiler error:
```
Invalid redeclaration of 'cancelButton'
```
## Root Cause
Multiple forms within the same feature area (e.g., Task) had cancel buttons, and they were all named `cancelButton`:
- `Task.cancelButton` (line 103) - for TaskForm
- `Task.cancelButton` (line 111) - for TaskDetail
This occurred in multiple structs:
- `Authentication.cancelButton` (Register)
- `Residence.cancelButton` (Form)
- `Task.cancelButton` (Form) ❌ Duplicate
- `Task.cancelButton` (Detail) ❌ Duplicate
- `Contractor.cancelButton` (Form)
- `Document.cancelButton` (Form)
- `Alert.cancelButton` (Generic alerts)
## Solution
Renamed context-specific cancel buttons to be more descriptive:
### Authentication Struct
```swift
// Before
static let cancelButton = "Register.CancelButton"
// After
static let registerCancelButton = "Register.CancelButton"
```
### Residence Struct
```swift
// Before
static let cancelButton = "ResidenceForm.CancelButton"
// After
static let formCancelButton = "ResidenceForm.CancelButton"
```
### Task Struct
```swift
// Before (DUPLICATE ERROR)
static let cancelButton = "TaskForm.CancelButton" // Line 103
static let cancelButton = "TaskDetail.CancelButton" // Line 111
// After (FIXED)
static let formCancelButton = "TaskForm.CancelButton"
static let detailCancelButton = "TaskDetail.CancelButton"
```
### Contractor Struct
```swift
// Before
static let cancelButton = "ContractorForm.CancelButton"
// After
static let formCancelButton = "ContractorForm.CancelButton"
```
### Document Struct
```swift
// Before
static let cancelButton = "DocumentForm.CancelButton"
// After
static let formCancelButton = "DocumentForm.CancelButton"
```
### Alert Struct (Unchanged)
```swift
// Generic cancel button for alerts - no conflict
static let cancelButton = "Alert.CancelButton"
```
## Files Modified
1. **`iosApp/Helpers/AccessibilityIdentifiers.swift`**
- Renamed 6 cancel button identifiers to be context-specific
2. **`iosApp/HoneyDueTests/ComprehensiveResidenceTests.swift`**
- Updated reference from `Residence.cancelButton``Residence.formCancelButton`
## Usage Examples
### Before (Would cause conflicts)
```swift
// Task Form
Button("Cancel") { }
.accessibilityIdentifier(AccessibilityIdentifiers.Task.cancelButton) // Which one?
// Task Detail
Button("Cancel") { }
.accessibilityIdentifier(AccessibilityIdentifiers.Task.cancelButton) // Duplicate!
```
### After (Clear and unambiguous)
```swift
// Task Form
Button("Cancel") { }
.accessibilityIdentifier(AccessibilityIdentifiers.Task.formCancelButton) // Clear
// Task Detail
Button("Cancel") { }
.accessibilityIdentifier(AccessibilityIdentifiers.Task.detailCancelButton) // Clear
```
## Naming Convention
For cancel buttons in different contexts:
- **Forms**: `formCancelButton`
- **Detail/View screens**: `detailCancelButton`
- **Registration**: `registerCancelButton`
- **Generic alerts**: `cancelButton` (in Alert struct)
## Test Updates Required
When adding identifiers to views, use the updated names:
```swift
// Residence Form
Button("Cancel") { dismiss() }
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.formCancelButton)
// Task Form
Button("Cancel") { dismiss() }
.accessibilityIdentifier(AccessibilityIdentifiers.Task.formCancelButton)
// Task Detail
Button("Cancel Task") { }
.accessibilityIdentifier(AccessibilityIdentifiers.Task.detailCancelButton)
```
## Verification
The file should now compile without errors. To verify:
```bash
cd /Users/treyt/Desktop/code/HoneyDue/HoneyDueKMM/iosApp
xcodebuild -project iosApp.xcodeproj -scheme iosApp -destination 'generic/platform=iOS Simulator' clean build
```
Or simply build in Xcode (⌘+B).
## Status
**Fixed** - All duplicate identifiers resolved
**Test files updated** - ComprehensiveResidenceTests uses new names
**No breaking changes** - Only internal identifier names changed, not the actual string values
---
**Last Updated:** November 18, 2025
**Issue:** Duplicate cancelButton declarations
**Resolution:** Renamed to context-specific names (formCancelButton, detailCancelButton, etc.)

View File

@@ -1,205 +0,0 @@
# HoneyDue iOS Design System
## Overview
This document outlines the modern, sleek design system implemented for the HoneyDue iOS app.
## Design Philosophy
- **Modern & Clean**: Minimalist approach with ample white space
- **Consistent**: Reusable components with unified styling
- **Accessible**: High contrast ratios and readable typography
- **Delightful**: Subtle animations and gradient accents
## Color Palette
### Primary Colors
- **Primary**: `#2563EB` - Modern blue for primary actions
- **Primary Light**: `#3B82F6` - Lighter variant for gradients
- **Primary Dark**: `#1E40AF` - Darker variant for pressed states
### Accent Colors
- **Accent**: `#8B5CF6` - Purple for special highlights
- **Accent Light**: `#A78BFA` - Lighter purple for gradients
### Semantic Colors
- **Success**: `#10B981` - Green for completed/success states
- **Warning**: `#F59E0B` - Orange for in-progress/warning states
- **Error**: `#EF4444` - Red for errors and destructive actions
- **Info**: `#3B82F6` - Blue for informational content
### Neutral Colors
- **Background**: `#F9FAFB` - Light gray for app background
- **Surface**: `#FFFFFF` - White for cards and surfaces
- **Surface Secondary**: `#F3F4F6` - Light gray for secondary surfaces
- **Text Primary**: `#111827` - Near black for primary text
- **Text Secondary**: `#6B7280` - Medium gray for secondary text
- **Text Tertiary**: `#9CA3AF` - Light gray for tertiary text
- **Border**: `#E5E7EB` - Light gray for borders
- **Border Light**: `#F3F4F6` - Very light gray for subtle borders
## Typography
### Display (Hero Sections)
- **Display Large**: 57pt, Bold, Rounded
- **Display Medium**: 45pt, Bold, Rounded
- **Display Small**: 36pt, Bold, Rounded
### Headline (Section Headers)
- **Headline Large**: 32pt, Bold, Rounded
- **Headline Medium**: 28pt, Semibold, Rounded
- **Headline Small**: 24pt, Semibold, Rounded
### Title (Card Titles)
- **Title Large**: 22pt, Semibold
- **Title Medium**: 18pt, Semibold
- **Title Small**: 16pt, Semibold
### Body (Main Content)
- **Body Large**: 17pt, Regular
- **Body Medium**: 15pt, Regular
- **Body Small**: 13pt, Regular
### Label (Labels & Captions)
- **Label Large**: 14pt, Medium
- **Label Medium**: 12pt, Medium
- **Label Small**: 11pt, Medium
### Caption
- **Caption**: 12pt, Regular
## Spacing Scale
- **XXS**: 4pt
- **XS**: 8pt
- **SM**: 12pt
- **MD**: 16pt
- **LG**: 24pt
- **XL**: 32pt
- **XXL**: 48pt
- **XXXL**: 64pt
## Border Radius
- **XS**: 4pt
- **SM**: 8pt
- **MD**: 12pt
- **LG**: 16pt
- **XL**: 20pt
- **XXL**: 24pt
- **Full**: 9999pt (Circular)
## Shadows
- **SM**: rgba(0,0,0,0.05), radius: 2, y: 1
- **MD**: rgba(0,0,0,0.1), radius: 4, y: 2
- **LG**: rgba(0,0,0,0.1), radius: 8, y: 4
- **XL**: rgba(0,0,0,0.15), radius: 16, y: 8
## Component Patterns
### Cards
- White background (`AppColors.surface`)
- Rounded corners (16pt or 20pt)
- Subtle shadow (`AppShadow.md` or `AppShadow.lg`)
- Padding: 16-32pt depending on content
### Buttons
- **Primary**: Gradient background, white text, 56pt height
- **Secondary**: Light gray background, primary color text, 56pt height
- Rounded corners: 12pt
- Pressed state: Scale to 0.98
### Text Fields
- Light gray background (`AppColors.surfaceSecondary`)
- 16pt padding
- 12pt rounded corners
- Focused state: Primary color border + subtle shadow
- Icon prefix for visual context
### Navigation Cards
- Gradient icon background in rounded rectangle
- Clear hierarchy: Title (semibold) + Subtitle (secondary color)
- Chevron indicator
- Hover/tap feedback
## Modernization Highlights
### Login Screen
- Circular gradient app icon with shadow
- Card-based form layout
- Animated focus states on input fields
- Gradient button with disabled states
- Clean error messaging
### Home Screen
- Personalized greeting header
- Stats overview card with icon badges
- Modern navigation cards with gradient icons
- Smooth scroll experience
### Components
- **OverviewCard**: Stats grid with dividers and icon badges
- **StatView**: Circular icon backgrounds with modern typography
- **HomeNavigationCard**: Gradient icons with clean layout
- **Design System**: Centralized colors, typography, and spacing
## Usage
### Importing the Design System
```swift
import SwiftUI
// Colors
.foregroundColor(AppColors.primary)
.background(AppColors.surface)
// Typography
.font(AppTypography.headlineSmall)
// Spacing
.padding(AppSpacing.md)
// Radius
.cornerRadius(AppRadius.lg)
// Shadows
.shadow(color: AppShadow.lg.color, radius: AppShadow.lg.radius, y: AppShadow.lg.y)
// Gradients
.fill(AppColors.primaryGradient)
```
### Button Styles
```swift
Button("Action") {
// action
}
.buttonStyle(PrimaryButtonStyle())
Button("Cancel") {
// action
}
.buttonStyle(SecondaryButtonStyle())
```
### Card Style
```swift
VStack {
// content
}
.cardStyle() // Applies background, corners, and shadow
```
## Future Enhancements
- Dark mode support with adaptive colors
- Additional component styles (chips, badges, alerts)
- Animation utilities for transitions
- Accessibility utilities (dynamic type, VoiceOver)
- Custom SF Symbols integration
## Migration Guide
When updating existing views:
1. Replace hardcoded colors with `AppColors.*`
2. Replace hardcoded fonts with `AppTypography.*`
3. Replace hardcoded spacing with `AppSpacing.*`
4. Use `cardStyle()` modifier for cards
5. Apply button styles for consistency
6. Add gradients to prominent UI elements
7. Ensure proper spacing hierarchy

View File

@@ -1,183 +0,0 @@
# Fix: Test Target Configuration
## Problem
When compiling tests, you're seeing:
```
@testable import iosApp
No such module 'iosApp'
```
This means the test target (`HoneyDueTests`) is not properly configured to access the main app target (`iosApp`).
## Solution: Configure Test Target in Xcode
### Step 1: Open Xcode Project
```bash
cd /Users/treyt/Desktop/code/HoneyDue/HoneyDueKMM/iosApp
open iosApp.xcodeproj
```
### Step 2: Add Target Dependency
1. **Select the project** in the Project Navigator (top item, blue icon)
2. **Select `HoneyDueTests` target** in the targets list (middle column)
3. **Go to "Build Phases" tab** (top of editor)
4. **Expand "Dependencies" section**
5. **Click the "+" button** under Dependencies
6. **Select `iosApp`** from the list
7. **Click "Add"**
### Step 3: Configure Test Host
1. Still in **`HoneyDueTests` target** → **Build Settings** tab
2. **Search for "Test Host"**
3. Set **Test Host** to:
```
$(BUILT_PRODUCTS_DIR)/iosApp.app/iosApp
```
4. **Search for "Bundle Loader"**
5. Set **Bundle Loader** to:
```
$(TEST_HOST)
```
### Step 4: Enable Testability
1. **Select `iosApp` target** (main app)
2. Go to **Build Settings** tab
3. **Search for "Enable Testability"**
4. Set **Enable Testability** to **YES** for **Debug** configuration
- (Leave it NO for Release)
### Step 5: Verify Module Name
1. **Select `iosApp` target**
2. Go to **Build Settings** tab
3. **Search for "Product Module Name"**
4. Verify it says **`iosApp`**
- If it's different (e.g., "HoneyDue"), you need to update your imports
### Step 6: Clean and Build
1. **Product** → **Clean Build Folder** (or press `⌘ + Shift + K`)
2. **Product** → **Build For** → **Testing** (or press `⌘ + Shift + U`)
### Step 7: Verify Fix
Open any test file (e.g., `ContractorViewModelTests.swift`) and verify the import works:
```swift
@testable import iosApp // Should no longer show error
```
---
## Alternative: Command Line Fix (Advanced)
If you prefer command-line configuration, you can use `xcodebuild` with PlistBuddy, but the Xcode GUI method above is safer and recommended.
---
## Visual Guide
### What It Should Look Like:
**HoneyDueTests Target → Build Phases → Dependencies:**
```
✅ iosApp (target)
```
**HoneyDueTests Target → Build Settings:**
```
Test Host: $(BUILT_PRODUCTS_DIR)/iosApp.app/iosApp
Bundle Loader: $(TEST_HOST)
```
**iosApp Target → Build Settings:**
```
Enable Testability: Yes (Debug only)
Product Module Name: iosApp
```
---
## Common Issues & Solutions
### Issue 1: Still getting "No such module"
**Solution:**
- Clean build folder (`⌘ + Shift + K`)
- Delete derived data:
```bash
rm -rf ~/Library/Developer/Xcode/DerivedData/iosApp-*
```
- Restart Xcode
- Build again
### Issue 2: Module name is different
**Solution:**
- Check what the actual module name is:
- Select `iosApp` target → Build Settings → Product Module Name
- Update all test imports to match:
```swift
@testable import <ActualModuleName>
```
### Issue 3: "Target is not an application"
**Solution:**
- Make sure you selected the **main app target** (`iosApp`), not the extension target
- The Test Host should point to the `.app` bundle
### Issue 4: Xcode can't find the app
**Solution:**
- Build the main app first: `⌘ + B`
- Then build tests: `⌘ + Shift + U`
---
## Quick Verification Checklist
After making changes, verify:
- [ ] `HoneyDueTests` target has `iosApp` in Dependencies
- [ ] Test Host is set to `$(BUILT_PRODUCTS_DIR)/iosApp.app/iosApp`
- [ ] Bundle Loader is set to `$(TEST_HOST)`
- [ ] `iosApp` target has "Enable Testability" = YES (Debug)
- [ ] Product Module Name matches your import statement
- [ ] Project builds successfully (`⌘ + B`)
- [ ] Tests build successfully (`⌘ + Shift + U`)
- [ ] No import errors in test files
---
## Why This Happens
The test target needs explicit configuration to:
1. **Know which app to test** (Target Dependency)
2. **Where to find the app binary** (Test Host)
3. **Access internal/private code** (Enable Testability + @testable import)
Without these settings, the compiler doesn't know that `iosApp` module exists.
---
## After Fixing
Once configured, your tests will:
- ✅ Import the main app module successfully
- ✅ Access internal classes and methods with `@testable import`
- ✅ Run against the actual app binary
- ✅ Have access to all app code for testing
---
**Estimated Time:** 2-3 minutes
**Difficulty:** Easy (GUI-based)
**Risk:** Low (non-destructive changes)
---
**Last Updated:** November 18, 2025
**Issue:** Test target not configured for app module access
**Resolution:** Add target dependency and configure test host in Xcode

View File

@@ -1,78 +0,0 @@
# Fix HoneyDueTests Target Configuration
## The Problem
The tests are failing with "No target application path specified" because the test target's `TEST_HOST` setting is hardcoded to a wrong path:
```
TEST_HOST = /Users/treyt/Desktop/code/HoneyDue/HoneyDueKMM/iosApp/build/Release-iphoneos/HoneyDue.app//HoneyDue
```
This path doesn't exist when running tests in Debug mode on the simulator.
## The Fix (Manual - Do This in Xcode)
1. **Open the project in Xcode:**
```bash
cd /Users/treyt/Desktop/code/HoneyDue/HoneyDueKMM/iosApp
open iosApp.xcodeproj
```
2. **Select the HoneyDueTests target:**
- Click on the project in the Project Navigator (blue icon at top)
- Select **HoneyDueTests** from the TARGETS list
3. **Go to Build Settings:**
- Click the **Build Settings** tab
- Make sure "All" and "Combined" are selected (not "Basic" or "Customized")
4. **Search for "TEST_HOST":**
- Use the search box at top right
- Type "TEST_HOST"
5. **Set TEST_HOST value:**
- Double-click the value field
- Change from:
```
/Users/treyt/Desktop/code/HoneyDue/HoneyDueKMM/iosApp/build/Release-iphoneos/HoneyDue.app//HoneyDue
```
- To:
```
$(BUILT_PRODUCTS_DIR)/HoneyDue.app/HoneyDue
```
- Press Enter
6. **Verify BUNDLE_LOADER:**
- Clear the search, search for "BUNDLE_LOADER"
- It should be set to:
```
$(TEST_HOST)
```
- If not, set it to that value
7. **Clean and rebuild:**
- Product → Clean Build Folder (Cmd+Shift+K)
- Product → Build (Cmd+B)
8. **Run tests:**
- Product → Test (Cmd+U)
- Or click the diamond icon next to any test method
## Verification
After making these changes, run:
```bash
xcodebuild -project iosApp.xcodeproj -target HoneyDueTests -showBuildSettings | grep TEST_HOST
```
Should output:
```
TEST_HOST = $(BUILT_PRODUCTS_DIR)/HoneyDue.app/HoneyDue
```
NOT a hardcoded absolute path.
## Why This Happened
The test target was likely created manually or the project was moved, causing Xcode to lose the proper build settings reference.

View File

@@ -1,164 +0,0 @@
# Failing Suites 0-3: Coverage + Rebuild Plan
## Baseline (from observed runs)
- `Suite0_OnboardingTests`: 1 test, 1 failure
- `Suite1_RegistrationTests`: 11 tests, 5 failures
- `Suite2_AuthenticationTests`: 6 tests, 2 failures
- `Suite3_ResidenceTests`: 6 tests, 6 failures
Primary failure logs used:
- `/tmp/ui_suite0.log`
- `/tmp/ui_suites_1_3.log`
---
## Suite0
### Failing test
- `Suite0_OnboardingTests.test_onboarding`
### What it is testing
- End-to-end onboarding progression from welcome/login entry into account creation and onward.
- UI interaction stability during onboarding form entry.
### Observed failure point
- Assertion failure: `Email field must become focused for typing`.
### Rebuild in new arch
Create a new test case focused on deterministic onboarding field interaction:
- `Onboarding_EmailRegistration_FocusAndInputFlow`
Coverage to preserve:
- Email field reliably focusable and typeable.
- Continue action only enabled after valid required inputs.
- Onboarding progresses to next state after valid submission.
Required infra:
- `OnboardingScreen` page object with `tapEmailField()`, `typeEmail()`, `assertEmailFieldFocused()`.
- Keyboard/overlay helper centralized (not inline in tests).
---
## Suite1
Detailed plan already captured in:
- `/Users/treyt/Desktop/code/HoneyDueKMM/iosApp/HoneyDueUITests/Docs/Suite1_Failing_Test_Rebuild_Plan.md`
### Failing tests
- `test07_successfulRegistrationAndVerification`
- `test09_registrationWithInvalidVerificationCode`
- `test10_verificationCodeFieldValidation`
- `test11_appRelaunchWithUnverifiedUser`
- `test12_logoutFromVerificationScreen`
### Rebuild targets
- `Registration_HappyPath_CompletesVerification_ThenCanLogout`
- `Registration_InvalidVerifyCode_ShowsError_StaysUnverified`
- `Registration_IncompleteVerifyCode_DoesNotVerify`
- `Registration_UnverifiedUser_RelaunchStillBlockedFromMain`
- `Registration_VerificationScreenLogout_ReturnsToLogin`
---
## Suite2
### Failing tests
- `Suite2_AuthenticationTests.test02_loginWithValidCredentials`
- `Suite2_AuthenticationTests.test06_logout`
### What they are testing
#### `test02_loginWithValidCredentials`
- Valid login path transitions from login screen to main app.
- Authenticated state exposes main navigation (tab bar/app root).
#### `test06_logout`
- Logged-in user can logout.
- Session is cleared and app returns to login state.
### Observed failure points
- `test02`: `Should navigate to main app after successful login`
- `test06`: `Should be logged in` (precondition for logout flow failed)
### Rebuild in new arch
Create explicit state-driven auth tests:
- `Auth_ValidLogin_TransitionsToMainApp`
- `Auth_Logout_FromMainApp_ReturnsToLogin`
Coverage to preserve:
- Login success sets authenticated UI state.
- Logout always clears authenticated state.
- No false-positive “logged in” assumptions.
Required infra:
- `LoginScreen`, `MainTabScreen`, `ProfileScreen` page objects.
- `AuthAssertions.assertAtLoginRoot()`, `assertAtMainRoot()`.
- Test user fixture policy for valid credentials.
---
## Suite3
### Failing tests
- `Suite3_ResidenceTests.test01_viewResidencesList`
- `Suite3_ResidenceTests.test02_navigateToAddResidence`
- `Suite3_ResidenceTests.test03_navigationBetweenTabs`
- `Suite3_ResidenceTests.test04_cancelResidenceCreation`
- `Suite3_ResidenceTests.test05_createResidenceWithMinimalData`
- `Suite3_ResidenceTests.test06_viewResidenceDetails`
### What they are testing
- Residence tab/list visibility.
- Navigation to add-residence form.
- Cross-tab navigation sanity.
- Canceling residence creation.
- Creating residence with minimal fields.
- Opening residence details.
### Observed failure pattern
All 6 fail at the same gateway:
- No `Residences` tab bar button match found.
- This indicates tests are not reaching authenticated main-app state before residence assertions.
### Rebuild in new arch
Split auth precondition from residence behavior:
- `Residence_Precondition_AuthenticatedAndAtResidencesTab`
- `Residence_OpenCreateForm`
- `Residence_CancelCreate_ReturnsToList`
- `Residence_CreateMinimal_ShowsInList`
- `Residence_OpenDetails_FromList`
- `Residence_TabNavigation_MainSections`
Coverage to preserve:
- Residence flows validated only after explicit `main app ready` assertion.
- Failures clearly classify as auth-gate vs residence-feature regression.
Required infra:
- `MainTabScreen.goToResidences()` with ID-first selectors.
- `ResidenceListScreen`, `ResidenceFormScreen`, `ResidenceDetailScreen` page objects.
- Shared precondition helper: `ensureAuthenticatedMainApp()`.
---
## Blueprint-aligned migration notes
- Keep old-to-new mapping explicit in PR description.
- Replace brittle text-based selectors with accessibility IDs first.
- Use one state assertion per transition boundary:
- `login -> verification -> main app -> login`.
- Move keyboard/strong-password overlay handling into one helper.
- Do not mark legacy tests removed until replacement coverage is green.
## Proposed replacement matrix
- `Suite0.test_onboarding` -> `Onboarding_EmailRegistration_FocusAndInputFlow`
- `Suite1.test07` -> `Registration_HappyPath_CompletesVerification_ThenCanLogout`
- `Suite1.test09` -> `Registration_InvalidVerifyCode_ShowsError_StaysUnverified`
- `Suite1.test10` -> `Registration_IncompleteVerifyCode_DoesNotVerify`
- `Suite1.test11` -> `Registration_UnverifiedUser_RelaunchStillBlockedFromMain`
- `Suite1.test12` -> `Registration_VerificationScreenLogout_ReturnsToLogin`
- `Suite2.test02` -> `Auth_ValidLogin_TransitionsToMainApp`
- `Suite2.test06` -> `Auth_Logout_FromMainApp_ReturnsToLogin`
- `Suite3.test01` -> `Residence_Precondition_AuthenticatedAndAtResidencesTab`
- `Suite3.test02` -> `Residence_OpenCreateForm`
- `Suite3.test03` -> `Residence_TabNavigation_MainSections`
- `Suite3.test04` -> `Residence_CancelCreate_ReturnsToList`
- `Suite3.test05` -> `Residence_CreateMinimal_ShowsInList`
- `Suite3.test06` -> `Residence_OpenDetails_FromList`

View File

@@ -1,174 +0,0 @@
# Suite1 Registration Failing Tests: Coverage + Rebuild Plan
## Scope
This document captures what the currently failing registration-flow tests are trying to validate and how to recreate that coverage using the new UI test architecture.
Source tests:
- `Suite1_RegistrationTests.test07_successfulRegistrationAndVerification`
- `Suite1_RegistrationTests.test09_registrationWithInvalidVerificationCode`
- `Suite1_RegistrationTests.test10_verificationCodeFieldValidation`
- `Suite1_RegistrationTests.test11_appRelaunchWithUnverifiedUser`
- `Suite1_RegistrationTests.test12_logoutFromVerificationScreen`
## Current Failure Context (Observed)
- Registration submit does not transition to a verification screen in automation runs.
- UI-level registration error shown during failures: `Password must be at least 8 characters`.
- Because registration transition fails, downstream verification assertions fail.
## What Each Failing Test Is Actually Testing
### 1) `test07_successfulRegistrationAndVerification`
Behavior intent:
- User can register with valid credentials.
- App transitions to verification state.
- Entering valid verification code completes verification.
- User lands in main app (tab bar available).
- Logout returns user to login.
Core business coverage:
- Happy-path onboarding/auth state progression.
- Verified user session gains app access.
- Logout clears authenticated session.
### 2) `test09_registrationWithInvalidVerificationCode`
Behavior intent:
- Registration reaches verification state.
- Entering wrong code shows verification error.
- User remains blocked from main app.
Core business coverage:
- Backend validation for invalid verification code.
- No false positive promotion to verified state.
### 3) `test10_verificationCodeFieldValidation`
Behavior intent:
- Verification screen enforces code format/length.
- Incomplete code does not complete verification.
- User remains on verification state.
Core business coverage:
- Client-side verification input guardrails.
- No bypass with partial code.
### 4) `test11_appRelaunchWithUnverifiedUser`
Behavior intent:
- User reaches unverified verification state.
- App terminate/relaunch preserves unverified gating.
- Relaunch must not allow direct main-app access.
Core business coverage:
- Session restore + auth gate correctness for unverified users.
### 5) `test12_logoutFromVerificationScreen`
Behavior intent:
- Unverified user can explicitly logout from verification screen.
- Verification UI dismisses.
- App returns to interactive login screen.
Core business coverage:
- Logout works from gated verification state.
- Session cleanup from pre-verified auth state.
## Rebuild These in New Architecture
## Shared Test Architecture Requirements
Create/ensure these reusable pieces:
- `AuthFlowHarness` (launch + auth preconditions + cleanup)
- `RegistrationScreen` page object
- `VerificationScreen` page object
- `MainTabScreen` page object
- `SessionStateAsserts` helpers for `login`, `verification`, `mainApp`
- `TestUserFactory` with deterministic unique users
Use stable selectors first:
- Accessibility IDs over title text.
- Support both auth/onboarding verification IDs only if product can route to either screen.
## Suggested New-Arch Test Cases (One-to-One Replacement)
### A. `Registration_HappyPath_CompletesVerification_ThenCanLogout`
Covers legacy test07.
Given:
- Fresh launch, logged out.
When:
- Register with valid user.
- Verify with valid code.
- Logout from profile/main app.
Then:
- Verification gate appears after register.
- Main app appears only after successful verify.
- Logout returns to login root.
### B. `Registration_InvalidVerifyCode_ShowsError_StaysUnverified`
Covers legacy test09.
Given:
- User registered and on verification screen.
When:
- Submit invalid verification code.
Then:
- Error banner/message visible.
- Verification screen remains active.
- Main app root not accessible.
### C. `Registration_IncompleteVerifyCode_DoesNotVerify`
Covers legacy test10.
Given:
- User on verification screen.
When:
- Enter fewer than required digits.
- Attempt verify (or assert button disabled).
Then:
- Verification completion does not occur.
- User remains blocked from main app.
### D. `Registration_UnverifiedUser_RelaunchStillBlockedFromMain`
Covers legacy test11.
Given:
- User registered but not verified.
When:
- Terminate and relaunch app.
Then:
- User is on verification gate (or login if session invalidated).
- User is never placed directly in main app state.
### E. `Registration_VerificationScreenLogout_ReturnsToLogin`
Covers legacy test12.
Given:
- User at verification gate.
When:
- Tap logout on verification screen.
Then:
- Verification state exits.
- Login root becomes active and interactive.
## Data + Environment Strategy for Rebuild
- Use API mode/environment that is stable for registration + verification in CI and local runs.
- Seed/fixture verification code contract must be explicit (example: fixed debug code).
- Generate unique username/email per test to avoid collisions.
- If keyboard autofill overlays are flaky, centralize handling in input helper (not per-test).
## Migration Notes
- Keep legacy tests disabled/removed only after each replacement test is green.
- Track replacement mapping in PR description:
- `old test -> new test`
- Preserve negative assertions ("must NOT access main app before verify").
## Open Risks To Resolve During Rebuild
- Registration password entry flakiness from iOS strong-password UI overlays.
- Potential mismatch between onboarding verification screen IDs and auth verification screen IDs.
- Environment-dependent backend behavior (local/dev) affecting registration transition.

View File

@@ -1,83 +0,0 @@
# honeyDue iOS UI Testing Architecture
## Directory Structure
```
HoneyDueUITests/
├── PageObjects/ # Screen abstractions (Page Object pattern)
│ ├── BaseScreen.swift # Common wait/assert utilities
│ ├── LoginScreen.swift # Login screen elements and actions
│ ├── RegisterScreen.swift # Registration screen
│ └── MainTabScreen.swift # Main tab navigation + settings + logout
├── TestConfiguration/ # Launch config, environment setup
│ └── TestLaunchConfig.swift
├── Fixtures/ # Test data builders
│ └── TestFixtures.swift
├── CriticalPath/ # Must-pass tests for CI gating
│ ├── SmokeTests.swift # Fast smoke suite (<2 min)
│ ├── AuthCriticalPathTests.swift # Auth flow validation
│ └── NavigationCriticalPathTests.swift # Tab + navigation validation
├── UITestHelpers.swift # Shared login/logout/navigation helpers
├── AccessibilityIdentifiers.swift # UI element IDs (synced with app-side copy)
└── README.md # This file
```
## Test Suites
| Suite | Purpose | CI Gate | Target Time |
|-------|---------|---------|-------------|
| SmokeTests | App launches, basic auth, tab existence | Every PR | <2 min |
| AuthCriticalPathTests | Login, logout, registration entry, forgot password | Every PR | <3 min |
| NavigationCriticalPathTests | Tab navigation, settings, add buttons | Every PR | <3 min |
## Patterns
### Page Object Pattern
Every screen has a corresponding PageObject in `PageObjects/`. Use these instead of raw XCUIElement queries in tests. Page objects encapsulate element lookups and common actions, making tests more readable and easier to maintain when the UI changes.
### Wait Helpers
NEVER use `sleep()` or `Thread.sleep()`. Use `waitForElement()`, `waitForElementToDisappear()`, `waitForHittable()`, or `waitForCondition()` from BaseScreen. These are condition-based waits that return as soon as the condition is met, making tests both faster and more reliable.
### Test Data
Use `TestFixtures` builders for consistent, unique test data. Random numbers and UUIDs ensure test isolation so tests can run in any order without interfering with each other.
### Launch Configuration
Use `TestLaunchConfig.launchApp()` for standard launches. Use `launchAuthenticated()` to skip login when the app supports test authentication bypass. The standard configuration disables animations and forces English locale.
### Accessibility Identifiers
All interactive elements must have identifiers defined in `AccessibilityIdentifiers.swift`. Use `.accessibilityIdentifier()` in SwiftUI views. Page objects reference these identifiers for element lookup. The test-side copy must stay in sync with the app-side copy at `iosApp/Helpers/AccessibilityIdentifiers.swift`.
## CI Configuration
### Critical Path (every PR)
```bash
xcodebuild test -project iosApp.xcodeproj -scheme iosApp \
-sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 17' \
-only-testing:HoneyDueUITests/SmokeTests \
-only-testing:HoneyDueUITests/AuthCriticalPathTests \
-only-testing:HoneyDueUITests/NavigationCriticalPathTests
```
### Full Regression (nightly)
```bash
xcodebuild test -project iosApp.xcodeproj -scheme iosApp \
-sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 17' \
-only-testing:HoneyDueUITests
```
## Flake Reduction
- Target: <2% flake rate on critical-path suite
- All waits use condition-based predicates (zero fixed sleeps)
- Test data uses unique identifiers to prevent cross-test interference
- UI animations disabled via launch arguments
- Element lookups use accessibility identifiers exclusively
## Adding New Tests
1. If the screen does not have a page object yet, create one in `PageObjects/` that extends `BaseScreen`.
2. Define accessibility identifiers in `AccessibilityIdentifiers.swift` for any new UI elements.
3. Sync the app-side copy of `AccessibilityIdentifiers.swift` with matching identifiers.
4. Add test data builders to `TestFixtures.swift` if needed.
5. Write the test in `CriticalPath/` for must-pass CI tests.
6. Verify zero `sleep()` calls before merging.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,254 +0,0 @@
# Task Summary Card - iOS Usage Guide
## Overview
The `TaskSummaryCard` SwiftUI component displays task statistics dynamically based on backend data. All metadata (icons, colors, display names) comes from the API.
## Files Created
1. **`Components/TaskSummaryCard.swift`** - Main component
**Note:** The component uses the existing `Color(hex:)` initializer from `Design/DesignSystem.swift`, so no additional files are needed.
## Basic Usage
### Example 1: Show All Categories
In `ResidenceDetailView.swift` or `ResidencesListView.swift`:
```swift
import SwiftUI
import Shared
struct ResidenceDetailView: View {
let residence: ResidenceSummary
var body: some View {
ScrollView {
VStack(spacing: 16) {
// Residence info...
// Task summary with all categories
TaskSummaryCard(taskSummary: residence.taskSummary)
.padding(.horizontal)
}
}
}
}
```
### Example 2: Show Only Specific Categories
For the main residence list, show only key categories:
```swift
struct ResidenceCard: View {
let residence: ResidenceSummary
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text(residence.name)
.font(.title2)
.fontWeight(.bold)
Text(residence.streetAddress)
.font(.subheadline)
.foregroundColor(.secondary)
// Only show overdue, current, and in progress
TaskSummaryCard(
taskSummary: residence.taskSummary,
visibleCategories: [
"overdue_tasks",
"current_tasks",
"in_progress_tasks"
]
)
}
.padding()
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(radius: 2)
}
}
```
### Example 3: In Home Screen
```swift
struct HomeScreenView: View {
@StateObject private var viewModel = ResidenceViewModel()
var body: some View {
ScrollView {
VStack(spacing: 20) {
ForEach(viewModel.residences, id: \.id) { residence in
VStack(alignment: .leading, spacing: 12) {
NavigationLink(destination: ResidenceDetailView(residence: residence)) {
VStack(alignment: .leading) {
Text(residence.name)
.font(.headline)
// Show task summary for quick overview
TaskSummaryCard(
taskSummary: residence.taskSummary,
visibleCategories: ["overdue_tasks", "current_tasks"]
)
}
}
}
.padding()
}
}
}
}
}
```
## Available Categories
The backend provides these categories (can filter with `visibleCategories`):
1. **overdue_tasks** - Red (#FF3B30) - `exclamationmark.triangle`
2. **current_tasks** - Blue (#007AFF) - `calendar`
3. **in_progress_tasks** - Orange (#FF9500) - `play.circle`
4. **backlog_tasks** - Purple (#5856D6) - `tray`
5. **done_tasks** - Green (#34C759) - `checkmark.circle`
6. **archived_tasks** - Gray (#8E8E93) - `archivebox`
## Customization
### Custom Styling
```swift
TaskSummaryCard(taskSummary: taskSummary)
.padding()
.background(Color(.secondarySystemBackground))
.cornerRadius(16)
.shadow(color: Color.black.opacity(0.2), radius: 8, x: 0, y: 4)
```
### Filter Specific Categories
```swift
// Show only active tasks
TaskSummaryCard(
taskSummary: taskSummary,
visibleCategories: [
"overdue_tasks",
"current_tasks",
"in_progress_tasks",
"backlog_tasks"
]
)
```
### Access Individual Category Counts
If you need to access counts programmatically:
```swift
extension TaskSummary {
func categoryCount(for name: String) -> Int {
categories.first { $0.name == name }?.count ?? 0
}
}
// Usage:
let overdueCount = taskSummary.categoryCount(for: "overdue_tasks")
```
## Component Features
**Dynamic Icons** - Uses SF Symbols from backend (e.g., `calendar`, `exclamationmark.triangle`)
**Dynamic Colors** - Parses hex colors from backend
**Flexible Filtering** - Show only categories you need
**Live Preview** - Includes SwiftUI preview with mock data
**Responsive** - Adapts to different screen sizes
**Native Look** - Uses iOS design patterns and typography
## Preview
The component includes a preview for development. In Xcode:
1. Open `Components/TaskSummaryCard.swift`
2. Click "Resume" in the preview canvas
3. See both "all categories" and "filtered" versions
## Integration Example
Here's a complete example for ResidenceDetailView:
```swift
import SwiftUI
import Shared
struct ResidenceDetailView: View {
let residence: ResidenceSummary
@State private var showingManageUsers = false
@State private var showingTasks = false
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 20) {
// Header
VStack(alignment: .leading, spacing: 8) {
Text(residence.name)
.font(.title)
.fontWeight(.bold)
Text(residence.streetAddress)
.font(.subheadline)
.foregroundColor(.secondary)
}
.padding(.horizontal)
// Task Summary Card
TaskSummaryCard(
taskSummary: residence.taskSummary,
visibleCategories: [
"overdue_tasks",
"current_tasks",
"in_progress_tasks"
]
)
.padding(.horizontal)
.onTapGesture {
showingTasks = true
}
// Other residence details...
}
.padding(.vertical)
}
.navigationTitle("Residence Details")
.navigationBarTitleDisplayMode(.inline)
.sheet(isPresented: $showingTasks) {
TasksView(residenceId: residence.id)
}
}
}
```
## Benefits
**Backend-Driven** - All icons, colors, names from API
**Single Source of Truth** - Change in `task/constants.py` updates everywhere
**Type-Safe** - Full Swift type safety with Shared models
**Reusable** - Use in any view that has TaskSummary
**Consistent** - Matches Android implementation
**Maintainable** - No hardcoded values
## Troubleshooting
### Colors not showing correctly
Make sure `Extensions/Color+Hex.swift` is included in your target.
### Icons not appearing
The component uses SF Symbols. Ensure the icon names from backend are valid SF Symbol names.
### Type errors
Make sure you're importing `Shared` module which contains the TaskSummary models.
### Preview not working
The preview uses mock data. If you get compiler errors, make sure all model types are properly imported.

View File

@@ -1,316 +0,0 @@
# UI Test Failures Analysis
## Status: Tests are failing as expected ✅
The UI tests you're seeing fail are **failing for the right reason** - they cannot find the UI elements because **accessibility identifiers haven't been added to the views yet**.
## What We've Completed
1.**Created comprehensive test suite** (34 tests)
- ComprehensiveAuthenticationTests.swift (11 tests)
- ComprehensiveResidenceTests.swift (10 tests)
- ComprehensiveTaskTests.swift (13 tests)
2.**Created centralized AccessibilityIdentifiers.swift**
- All identifiers defined and organized by feature
- Fixed duplicate identifier issues
- Ready to be used in views
3.**Fixed test compilation issues**
- Added `@testable import HoneyDue` to all test files
- Fixed ambiguous type references
- Tests compile successfully
4.**Created comprehensive documentation**
- XCUITEST_IMPLEMENTATION_GUIDE.md
- XCUITEST_IMPLEMENTATION_SUMMARY.md
- ACCESSIBILITY_IDENTIFIERS_FIX.md
- FIX_TEST_TARGET.md
## What Still Needs to Be Done
### Critical: Add Accessibility Identifiers to Views
The tests are **correctly written** but will fail until you add the accessibility identifiers to the actual SwiftUI views. Here's what needs to be done:
### High Priority Views (Required for tests to pass)
#### 1. Authentication Views
**File**: `iosApp/Login/LoginView.swift`
```swift
TextField("Enter your email", text: $viewModel.username)
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.usernameField)
SecureField("Password", text: $viewModel.password)
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.passwordField)
Button("Login") { }
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.loginButton)
Button("Sign Up") { }
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.signUpButton)
```
**File**: `iosApp/Register/RegisterView.swift`
```swift
TextField("Username", text: $username)
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.registerUsernameField)
TextField("Email", text: $email)
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.registerEmailField)
SecureField("Password", text: $password)
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.registerPasswordField)
SecureField("Confirm Password", text: $confirmPassword)
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.registerConfirmPasswordField)
Button("Register") { }
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.registerButton)
```
#### 2. Navigation (Tab Bar)
**File**: `iosApp/MainTabView.swift`
```swift
.tabItem {
Label("Residences", systemImage: "house.fill")
}
.accessibilityIdentifier(AccessibilityIdentifiers.Navigation.residencesTab)
.tabItem {
Label("Tasks", systemImage: "checklist")
}
.accessibilityIdentifier(AccessibilityIdentifiers.Navigation.tasksTab)
.tabItem {
Label("Contractors", systemImage: "person.2.fill")
}
.accessibilityIdentifier(AccessibilityIdentifiers.Navigation.contractorsTab)
.tabItem {
Label("Documents", systemImage: "doc.fill")
}
.accessibilityIdentifier(AccessibilityIdentifiers.Navigation.documentsTab)
.tabItem {
Label("Profile", systemImage: "person.fill")
}
.accessibilityIdentifier(AccessibilityIdentifiers.Navigation.profileTab)
```
#### 3. Residence Views
**File**: `iosApp/Residence/ResidencesView.swift`
```swift
Button { showingAddForm = true } label: {
Label("Add Property", systemImage: "plus")
}
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.addButton)
```
**File**: `iosApp/Residence/ResidenceFormView.swift`
```swift
TextField("Property Name", text: $name)
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.nameField)
Picker("Property Type", selection: $propertyType) { }
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.propertyTypePicker)
TextField("Street Address", text: $streetAddress)
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.streetAddressField)
TextField("City", text: $city)
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.cityField)
TextField("State/Province", text: $stateProvince)
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.stateProvinceField)
TextField("Postal Code", text: $postalCode)
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.postalCodeField)
TextField("Country", text: $country)
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.countryField)
TextField("Bedrooms", text: $bedrooms)
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.bedroomsField)
TextField("Bathrooms", text: $bathrooms)
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.bathroomsField)
Button("Save") { }
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.saveButton)
Button("Cancel") { }
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.formCancelButton)
```
**File**: `iosApp/Residence/ResidenceDetailView.swift`
```swift
Button("Edit") { }
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.editButton)
Button("Delete") { }
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.deleteButton)
```
#### 4. Task Views
**File**: `iosApp/Task/TasksView.swift`
```swift
Button { showingAddForm = true } label: {
Label("Add Task", systemImage: "plus")
}
.accessibilityIdentifier(AccessibilityIdentifiers.Task.addButton)
```
**File**: `iosApp/Task/TaskFormView.swift`
```swift
TextField("Task Title", text: $title)
.accessibilityIdentifier(AccessibilityIdentifiers.Task.titleField)
TextEditor(text: $description)
.accessibilityIdentifier(AccessibilityIdentifiers.Task.descriptionField)
Picker("Category", selection: $category) { }
.accessibilityIdentifier(AccessibilityIdentifiers.Task.categoryPicker)
Picker("Priority", selection: $priority) { }
.accessibilityIdentifier(AccessibilityIdentifiers.Task.priorityPicker)
Picker("Status", selection: $status) { }
.accessibilityIdentifier(AccessibilityIdentifiers.Task.statusPicker)
DatePicker("Due Date", selection: $dueDate)
.accessibilityIdentifier(AccessibilityIdentifiers.Task.dueDatePicker)
Button("Save") { }
.accessibilityIdentifier(AccessibilityIdentifiers.Task.saveButton)
Button("Cancel") { }
.accessibilityIdentifier(AccessibilityIdentifiers.Task.formCancelButton)
```
#### 5. Profile/Logout
**File**: `iosApp/Profile/ProfileView.swift`
```swift
Button("Logout") { }
.accessibilityIdentifier(AccessibilityIdentifiers.Profile.logoutButton)
```
#### 6. Alert Buttons
In any confirmation dialogs:
```swift
.alert("Delete Residence?", isPresented: $showingDeleteAlert) {
Button("Delete", role: .destructive) { }
.accessibilityIdentifier(AccessibilityIdentifiers.Alert.deleteButton)
Button("Cancel", role: .cancel) { }
.accessibilityIdentifier(AccessibilityIdentifiers.Alert.cancelButton)
}
```
## Quick Start: Add Identifiers to Critical Paths
To get the tests passing, start with these files **in order**:
1. **LoginView.swift** - username, password, login button
2. **RegisterView.swift** - all registration fields
3. **MainTabView.swift** - tab bar items
4. **ResidencesView.swift** - add button
5. **ResidenceFormView.swift** - all form fields
6. **TaskFormView.swift** - all form fields
7. **ProfileView.swift** - logout button
## How to Add Identifiers (Quick Reference)
For any UI element in SwiftUI, add:
```swift
.accessibilityIdentifier(AccessibilityIdentifiers.FeatureArea.elementName)
```
**Examples:**
```swift
// Text field
TextField("Email", text: $email)
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.usernameField)
// Button
Button("Save") { }
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.saveButton)
// Picker
Picker("Type", selection: $type) { }
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.propertyTypePicker)
// Tab item
.tabItem { Label("Tasks", systemImage: "checklist") }
.accessibilityIdentifier(AccessibilityIdentifiers.Navigation.tasksTab)
```
## Verifying Identifiers Are Working
After adding identifiers to a view, you can verify they're accessible:
1. **Run the app in Simulator**
2. **Open Accessibility Inspector** (Xcode → Open Developer Tool → Accessibility Inspector)
3. **Select the Simulator** in the Accessibility Inspector
4. **Hover over elements** to see if the identifier appears
Or just run the specific test:
```bash
xcodebuild test \
-project iosApp.xcodeproj \
-scheme iosApp \
-destination 'platform=iOS Simulator,name=iPhone 17' \
-only-testing:HoneyDueTests/ComprehensiveAuthenticationTests/testLoginWithValidCredentials
```
## Why Tests Are Failing Now
The tests look for elements like this:
```swift
let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.usernameField]
XCTAssertTrue(usernameField.exists) // FAILS - element not found
```
This fails because the actual `TextField` in `LoginView.swift` doesn't have:
```swift
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.usernameField)
```
Once you add the identifier, the test will find it:
```swift
let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.usernameField]
XCTAssertTrue(usernameField.exists) // PASSES - element found!
```
## Expected Timeline
- **Adding identifiers to 1 view file**: 2-5 minutes
- **Getting first test passing** (login): 10-15 minutes
- **Getting auth tests passing**: 30-45 minutes
- **Getting all critical tests passing**: 2-3 hours
- **Complete implementation** (all 30+ files): 6-8 hours
## Next Steps
1. **Start with LoginView.swift** - add the 4 critical identifiers (username, password, login, signUp)
2. **Run testLoginWithValidCredentials** - verify it finds the elements
3. **Add RegisterView.swift identifiers** - all registration fields
4. **Run testUserRegistrationComplete** - verify registration flow
5. **Continue systematically** through the view files
## Files That Need Identifiers (Complete List)
See `XCUITEST_IMPLEMENTATION_GUIDE.md` section "Views That Need Accessibility Identifiers" for the complete checklist of 30+ view files.
## Getting Help
If tests still fail after adding identifiers:
1. **Check spelling** - identifier names must match exactly
2. **Check scope** - make sure you're using the right struct (e.g., `Authentication.loginButton` not `Login.loginButton`)
3. **Check element type** - textFields vs secureTextFields vs buttons
4. **Run Accessibility Inspector** - verify the identifier is actually set
---
**Bottom Line**: The test infrastructure is complete and working correctly. Tests fail because views need accessibility identifiers. Once you add identifiers to views, tests will pass. Start with LoginView.swift and work through the critical path.

View File

@@ -1,236 +0,0 @@
# HoneyDue iOS UI Tests
## ✅ Status: WORKING
All UI tests have been rewritten using a working, verified pattern. Tests are organized by feature and use flexible selectors for stability.
## 📁 Test Files
### SimpleLoginTest.swift (2 tests)
Foundation tests that verify basic app functionality:
- `testAppLaunchesAndShowsLoginScreen()` - App launches and login UI appears
- `testCanTypeInLoginFields()` - User can interact with login form
### AuthenticationTests.swift (6 tests)
Authentication and user session management:
- `testLoginWithValidCredentials()` - Successful login flow
- `testLoginWithInvalidCredentials()` - Error handling for bad credentials
- `testPasswordVisibilityToggle()` - Password show/hide functionality
- `testNavigationToSignUp()` - Navigate to registration screen
- `testForgotPasswordNavigation()` - Navigate to password reset
- `testLogout()` - Complete logout flow
### ResidenceTests.swift (6 tests)
Property/residence management:
- `testViewResidencesList()` - View residences or empty state
- `testNavigateToAddResidence()` - Open add residence form and verify all required fields
- `testCreateResidenceWithMinimalData()` - Create new property with required fields (includes property type selection)
- `testCancelResidenceCreation()` - Cancel form without saving
- `testViewResidenceDetails()` - View property details
- `testNavigationBetweenTabs()` - Tab navigation works
### TaskTests.swift (7 tests)
Task management functionality:
- `testViewTasksList()` - View tasks or empty state
- `testNavigateToAddTask()` - Open add task form
- `testCreateBasicTask()` - Create new task
- `testCancelTaskCreation()` - Cancel form without saving
- `testViewTaskDetails()` - View task details
- `testNavigateToContractors()` - Navigate to contractors tab
- `testNavigateToDocuments()` - Navigate to documents tab
## 🎯 Test Design Principles
### 1. Logout/Login Before Tests
- **SimpleLoginTest** and **AuthenticationTests**: Start logged out (call `ensureLoggedOut()`)
- **ResidenceTests** and **TaskTests**: Start logged in (call `ensureLoggedIn()`)
### 2. Flexible Selectors
Tests use `NSPredicate` with `CONTAINS[c]` for case-insensitive partial matches:
```swift
// Fragile (breaks if text changes)
app.buttons["Sign In"]
// Robust (works with variations)
app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'Sign In'")).firstMatch
```
### 3. Proper Waits
Uses `waitForExistence(timeout:)` instead of `sleep()` where possible:
```swift
XCTAssertTrue(element.waitForExistence(timeout: 10), "Element should appear")
```
### 4. Test Independence
- Each test creates unique data using timestamps
- Tests don't depend on execution order
- Cleanup happens automatically via `tearDown()`
### 5. No Graceful Passes - Tests Must Fail When They Should
Tests are designed to FAIL when prerequisites aren't met, not pass gracefully:
```swift
// WRONG - Graceful pass (always passes)
if !addButton.exists {
XCTAssertTrue(true, "Skipping - requires residence")
return
}
// CORRECT - Meaningful failure
let addButton = app.buttons["Task.AddButton"]
XCTAssertTrue(addButton.waitForExistence(timeout: 5),
"Add task button must exist - create a residence first via ResidenceTests.testCreateResidenceWithMinimalData")
```
## 🚀 Running Tests
### In Xcode (Recommended)
1. Open `iosApp.xcodeproj`
2. Select **HoneyDueUITests** scheme
3. Press `Cmd+U` to run all tests
4. Or click diamond icon next to individual test to run just that one
### Command Line
```bash
# Run all UI tests
xcodebuild test -project iosApp.xcodeproj -scheme HoneyDueUITests \
-destination 'platform=iOS Simulator,name=iPhone 17'
# Run specific test file
xcodebuild test -project iosApp.xcodeproj -scheme HoneyDueUITests \
-destination 'platform=iOS Simulator,name=iPhone 17' \
-only-testing:HoneyDueUITests/AuthenticationTests
# Run specific test
xcodebuild test -project iosApp.xcodeproj -scheme HoneyDueUITests \
-destination 'platform=iOS Simulator,name=iPhone 17' \
-only-testing:HoneyDueUITests/AuthenticationTests/testLoginWithValidCredentials
```
## 📝 Test Credentials
Tests use these credentials (must exist in your test environment):
- **Username**: `testuser`
- **Password**: `TestPass123!`
Make sure this user exists in your backend before running tests.
## ✍️ Writing New Tests
### Pattern to Follow
```swift
import XCTest
final class YourTests: XCTestCase {
var app: XCUIApplication!
override func setUpWithError() throws {
continueAfterFailure = false
app = XCUIApplication()
app.launch()
// Choose one:
ensureLoggedOut() // For login/auth tests
ensureLoggedIn() // For feature tests
}
override func tearDownWithError() throws {
app = nil
}
// Copy helper methods from existing tests
func testYourFeature() {
// Given: Setup state
// When: User action
// Then: Verify result
XCTAssertTrue(condition, "Descriptive error message")
}
}
```
### Finding Elements
```swift
// Text fields
app.textFields.containing(NSPredicate(format: "placeholderValue CONTAINS[c] 'keyword'")).firstMatch
// Buttons
app.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'keyword'")).firstMatch
// Static text
app.staticTexts.containing(NSPredicate(format: "label CONTAINS[c] 'keyword'")).firstMatch
// Tab bar buttons
app.tabBars.buttons.containing(NSPredicate(format: "label CONTAINS[c] 'keyword'")).firstMatch
```
## 🐛 Troubleshooting
### Test Fails: "Element not found"
- Add `sleep(2)` before checking for element
- Increase timeout: `element.waitForExistence(timeout: 10)`
- Check if element actually exists in that state
- Use `app.debugDescription` to see all visible elements
### Test Fails: "Already logged in/out"
- Ensure `setUp()` calls correct helper (`ensureLoggedIn()` or `ensureLoggedOut()`)
- Check that logout/login logic is working
### Test Fails: "Residence/Task required"
- Some tests need data to exist first (e.g., testViewResidenceDetails requires testCreateResidenceWithMinimalData to run first)
- Tests will FAIL with meaningful error messages if prerequisites aren't met
- Error messages indicate which test needs to run first or what data is required
- Run tests in order or run the prerequisite test first
## 📊 Test Summary
| Test File | Tests | Focus |
|-----------|-------|-------|
| SimpleLoginTest | 2 | Foundation |
| AuthenticationTests | 6 | Login/Logout |
| ResidenceTests | 6 | Property Management |
| TaskTests | 7 | Task Management |
| **Total** | **21** | **Core Flows** |
## 🎉 Success!
These tests are:
- ✅ Actually working (verified)
- ✅ Based on SimpleLoginTest pattern that passed
- ✅ Using flexible selectors
- ✅ Following best practices
- ✅ Well-documented
- ✅ Independent and repeatable
-**NO GRACEFUL PASSES** - Tests fail meaningfully when they should
## 📋 Recent Changes (2025-11-19)
**Removed All Graceful Passes:**
- ✅ ResidenceTests: Removed graceful passes from `testViewResidencesList`, `testViewResidenceDetails`
- ✅ TaskTests: Removed graceful passes from `testNavigateToAddTask`, `testCreateBasicTask`, `testCancelTaskCreation`, `testViewTaskDetails`
- ✅ AuthenticationTests: Removed graceful pass from `testPasswordVisibilityToggle`
**Philosophy Change:**
- Tests now FAIL with meaningful error messages when prerequisites aren't met
- No more `XCTAssertTrue(true, "Skipping...")` patterns
- Error messages indicate exactly what's needed (e.g., "create a residence first via ResidenceTests.testCreateResidenceWithMinimalData")
**App Architecture Fix:**
- ✅ Created `RootView.swift` with `AuthenticationManager` singleton
- ✅ App now checks for token on launch and routes accordingly (MainTabView if authenticated, LoginView if not)
- ✅ No more flash of login screen when user is already authenticated
- ✅ Tests now start from correct state (logged in or logged out)
- ✅ Fixed `ensureLoggedOut()` to properly assert logout succeeded
- ✅ Fixed logout flow: `LoginViewModel.logout()` now calls `AuthenticationManager.shared.logout()`
- ✅ Removed `showMainTab` state variable - navigation now handled by `AuthenticationManager.isAuthenticated`
- ✅ LoginView accepts `onLoginSuccess` callback that notifies AuthenticationManager
- ✅ Logout properly returns to login screen without crashes
---
**Last Updated**: 2025-11-19
**Author**: Claude Code
**Status**: ✅ All tests working and fail meaningfully

View File

@@ -1,300 +0,0 @@
# HoneyDue iOS UI Tests - Complete Rewrite Summary
## ✅ Status: BUILD SUCCEEDED
All UI tests have been completely rewritten from scratch with comprehensive edge case coverage as requested.
## 📊 Test Files Created/Updated
### 1. **ComprehensiveAuthenticationTests.swift** (558 lines)
**Tests: 20**
- ✅ testUserRegistrationComplete
- ✅ testRegistrationWithExistingUsername
- ✅ testRegistrationWithInvalidEmail
- ✅ testRegistrationWithMismatchedPasswords
- ✅ testRegistrationWithEmptyFields
- ✅ testRegistrationWithWeakPassword
- ✅ testLoginWithValidCredentials
- ✅ testLoginWithInvalidCredentials
- ✅ testLoginWithEmptyUsername
- ✅ testLoginWithEmptyPassword
- ✅ testLoginWithEmptyFields
- ✅ testPasswordVisibilityToggle
- ✅ testLogout
- ✅ testLogoutClearsSession
- ✅ testForgotPasswordFlow
- ✅ testForgotPasswordWithValidEmail
- ✅ testNavigationBetweenLoginAndRegister
- ✅ testSessionPersistsAcrossAppRelaunch
**Edge Cases Covered:**
- Empty field validation
- Invalid email formats
- Password mismatch scenarios
- Weak password validation
- Invalid credentials handling
- Session persistence
- Navigation flows
### 2. **ComprehensiveResidenceTests.swift** (721 lines)
**Tests: 15**
- ✅ testCreateResidenceComplete
- ✅ testCreateResidenceWithMinimalData
- ✅ testCreateResidenceWithAllOptionalFields
- ✅ testCreateResidenceValidationRequired
- ✅ testCreateResidenceValidationMissingAddress
- ✅ testCreateResidenceCancellation
- ✅ testViewResidenceDetails
- ✅ testResidenceDetailShowsAllInfo
- ✅ testResidenceDetailShowsTasksSection
- ✅ testEditResidence
- ✅ testEditResidenceCancel
- ✅ testEditResidenceChangeAddress
- ✅ testDeleteResidence
- ✅ testDeleteResidenceCancellation
- ✅ testEmptyStateDisplayIfNoResidences
**Edge Cases Covered:**
- Complete vs minimal data entry
- All optional fields populated
- Required field validation
- Missing address validation
- Form cancellation
- Edit cancellation
- Delete confirmation/cancellation
- Empty state handling
- Unique timestamped data
### 3. **ComprehensiveTaskTests.swift** (708 lines)
**Tests: 16**
- ✅ testCreateOneTimeTaskComplete
- ✅ testCreateTaskWithMinimalData
- ✅ testCreateRecurringTask
- ✅ testCreateTaskWithAllFields
- ✅ testCreateTaskValidation
- ✅ testCreateTaskCancellation
- ✅ testMarkTaskInProgress
- ✅ testCompleteTask
- ✅ testCompleteTaskWithMinimalInfo
- ✅ testEditTask
- ✅ testEditTaskCancel
- ✅ testDeleteTask
- ✅ testDeleteTaskCancellation
- ✅ testKanbanViewDisplaysColumns
- ✅ testNavigateToTaskFromKanban
- ✅ testFilterTasksByResidence
**Edge Cases Covered:**
- One-time vs recurring tasks
- Minimal vs complete data
- Task status transitions
- Completion with/without details
- Edit cancellation
- Delete confirmation/cancellation
- Kanban column display
- Residence filtering
### 4. **TestHelpers.swift** (452 lines)
**Comprehensive Helper Methods:**
#### Authentication Helpers
- `login(username:password:)` - Performs login with validation
- `logout()` - Performs logout with screen verification
#### Navigation Helpers
- `navigateToTab(_:)` - Smart tab navigation with flexible naming
- `navigateBack()` - Back button navigation
#### Assertion Helpers
- `assertElementExists(_:timeout:message:)`
- `assertElementDoesNotExist(_:timeout:message:)`
- `assertNavigatedTo(title:timeout:)`
- `assertTextExists(_:timeout:)`
- `assertTextDoesNotExist(_:timeout:)`
#### Wait Helpers (Robust XCTest Expectations)
- `wait(seconds:)` - Simple delay
- `waitForElementToAppear(_:timeout:)` - Predicate-based wait
- `waitForElementToDisappear(_:timeout:)` - Predicate-based wait
- `waitForElementToBeHittable(_:timeout:)` - Hittable predicate
- `waitForElementValue(_:value:timeout:)` - Value predicate
- `waitForAnyElement(_:timeout:)` - First match from array
- `waitForLoadingToComplete(timeout:)` - Loading indicator wait
#### Interaction Helpers
- `scrollToElement(_:maxAttempts:)` - Auto-scroll to make hittable
- `scrollDownToFind(_:maxAttempts:)` - Scroll down search
- `scrollUpToFind(_:maxAttempts:)` - Scroll up search
- `clearTextField(_:)` - Robust text clearing
- `typeTextSlowly(_:into:delay:)` - Slow typing for problematic fields
- `tapWithRetry(_:maxAttempts:)` - Retry tapping
- `dismissKeyboard()` - Return key dismiss
- `dismissKeyboardByTappingOutside()` - Tap outside dismiss
#### Picker Helpers
- `selectPickerValue(_:value:)` - Wheel or menu picker
- `selectDate(_:year:month:day:)` - Date picker
#### Alert Helpers
- `dismissAlert(timeout:)` - OK button
- `confirmAlert(timeout:)` - Confirm button
- `cancelAlert(timeout:)` - Cancel button
#### Query Helpers
- `findElementByPartialText(_:elementType:)` - Flexible text search
- `findElements(matching:elementType:)` - Predicate search
- `hasErrorMessage()` - Error detection
- `isLoading()` - Loading indicator check
#### Debugging Helpers
- `printVisibleElements()` - Debug element dump
- `printElementHierarchy()` - Debug hierarchy
- `takeScreenshot(named:)` - Named screenshots
### 5. **AccessibilityIdentifiers.swift** (213 lines)
Copied to HoneyDueUITests folder for UI test access without `@testable import`.
### 6. **HoneyDueUITests.swift & HoneyDueUITestsLaunchTests.swift**
Updated to remove `@testable import HoneyDue` (UI tests run in separate process).
## 🗑️ Removed Files
- AuthenticationUITests.swift (old, had @testable import)
- TaskUITests.swift (old, had @testable import)
- ResidenceUITests.swift (old, had @testable import)
- MultiUserUITests.swift (old, had @testable import)
- DebugLoginTest.swift (old, had @testable import)
## ✨ Key Improvements
### 1. No `@testable import` Required
All tests run in their own process without accessing app internals, following proper UI testing practices.
### 2. Comprehensive Edge Case Coverage
Every test includes:
- ✅ Positive scenarios (happy path)
- ✅ Negative scenarios (error cases)
- ✅ Validation scenarios (empty fields, invalid data)
- ✅ Cancellation scenarios (user changes mind)
- ✅ Boundary scenarios (minimal vs maximal data)
### 3. Robust Wait Functions
- Uses XCTest NSPredicate expectations instead of simple sleeps
- Configurable timeouts
- Proper element state verification (exists, hittable, value)
### 4. Better Error Messages
All assertions include descriptive failure messages for easier debugging.
### 5. Reusable Helper Methods
Common operations extracted into TestHelpers base class to avoid duplication.
### 6. Flexible Element Finding
Tests use NSPredicate with case-insensitive contains to handle text variations.
### 7. Proper Test Isolation
Each test:
- Creates its own unique test data (using timestamps)
- Manages its own setup/teardown
- Doesn't depend on other tests
- Cleans up after itself
### 8. Given-When-Then Pattern
All tests follow clear structure:
```swift
// Given: Initial state setup
// When: User action
// Then: Expected outcome verification
// And: Additional verifications
```
## 📈 Test Coverage Summary
| Category | Tests | Edge Cases |
|----------|-------|------------|
| Authentication | 20 | Registration errors, login errors, session management |
| Residence Management | 15 | CRUD operations, validation, empty states |
| Task Management | 16 | Task types, status transitions, kanban views |
| **Total** | **51** | **Comprehensive coverage** |
## 🚀 Build Status
```
** BUILD SUCCEEDED **
```
All tests compile successfully with no errors. Only warnings are from the main app code, not test code.
## 📝 Next Steps for User
### In Xcode GUI (One-Time Setup Required):
1. **Fix AccessibilityIdentifiers Target Membership:**
- Open `iosApp.xcodeproj` in Xcode
- Select `iosApp/Helpers/AccessibilityIdentifiers.swift` in Project Navigator
- In File Inspector (right panel), **uncheck** "HoneyDueUITests" from Target Membership
- Only `iosApp` should be checked
- The copy in `HoneyDueUITests/AccessibilityIdentifiers.swift` should have "HoneyDueUITests" checked
2. **Run Tests:**
```bash
# Run all UI tests
xcodebuild test -project iosApp.xcodeproj -scheme iosApp \
-destination 'platform=iOS Simulator,name=iPhone 17' \
-only-testing:HoneyDueUITests
# Run specific test class
xcodebuild test -project iosApp.xcodeproj -scheme iosApp \
-destination 'platform=iOS Simulator,name=iPhone 17' \
-only-testing:HoneyDueUITests/ComprehensiveAuthenticationTests
# Run specific test
xcodebuild test -project iosApp.xcodeproj -scheme iosApp \
-destination 'platform=iOS Simulator,name=iPhone 17' \
-only-testing:HoneyDueUITests/ComprehensiveAuthenticationTests/testLoginWithValidCredentials
```
## 🎯 Test Philosophy
These tests follow Apple's UI Testing best practices:
1. **Black Box Testing**: Tests interact with the app as a user would
2. **No Internal Access**: No `@testable import`, ensuring tests verify actual user experience
3. **Robust Selectors**: Uses accessibility identifiers for stable element location
4. **Proper Waits**: Uses XCTest expectations instead of arbitrary sleeps
5. **Comprehensive Coverage**: Tests both success and failure paths
6. **Maintainable**: Clear naming, helper methods, and well-structured code
## 📚 Documentation
All test files include:
- Header comments describing purpose
- MARK comments organizing test sections
- Inline comments explaining complex logic
- Descriptive test names (no need to read code to understand what's tested)
## 🔍 Troubleshooting
If tests fail:
1. **Check app is testable**: Ensure accessibility identifiers are present in SwiftUI views
2. **Check test data**: Tests use unique timestamps to avoid conflicts
3. **Check timeouts**: Increase timeouts if app is slow (edit in TestHelpers.swift)
4. **Check simulator**: Ensure correct simulator is running and app is installed
5. **Use debug helpers**: Call `printVisibleElements()` or `takeScreenshot()` in tests
## ✅ Success Criteria Met
- ✅ All tests compile without errors
- ✅ Comprehensive edge case coverage for every flow
- ✅ No `@testable import` usage
- ✅ Robust wait functions implemented
- ✅ Reusable helper methods created
- ✅ Clear, maintainable test code
- ✅ Proper test isolation and cleanup
- ✅ 51 comprehensive UI tests covering authentication, residence, and task management
---
**Generated:** 2025-11-19
**Author:** Claude Code (Anthropic)
**Build Status:** ✅ BUILD SUCCEEDED

View File

@@ -1,73 +0,0 @@
# HoneyDue iOS UI Test Strategy
## Current Status: REBUILDING FROM SCRATCH
The previous comprehensive tests (50+ tests) were written without proper verification and are failing. We're now taking a methodical approach.
## Phase 1: Foundation Test (IN PROGRESS)
### SimpleLoginTest.swift
- ✅ Created with logout-first logic
- 🔄 Testing in progress
- Contains 2 basic tests:
1. `testAppLaunchesAndShowsLoginScreen()` - Verifies app launches and shows login UI
2. `testCanTypeInLoginFields()` - Verifies we can interact with username/password fields
**Key Feature**: `ensureLoggedOut()` helper automatically logs out before each test
## Phase 2: Build Working Tests Incrementally
Once SimpleLoginTest works, we'll build:
### Authentication Tests (Priority 1)
- Login with valid credentials
- Login with invalid credentials
- Logout flow
- Registration flow
- Password reset flow
### Residence Tests (Priority 2)
- View residences list
- Create residence
- Edit residence
- Delete residence
### Task Tests (Priority 3)
- View tasks
- Create task
- Mark task in progress
- Complete task
## Test Principles
1. **Always logout first** - Each test starts from login screen
2. **Use flexible selectors** - NSPredicate with CONTAINS instead of exact matches
3. **Wait for elements** - Use `waitForExistence(timeout:)` instead of `sleep()`
4. **Test one thing** - Each test focuses on a single user flow
5. **Clean up** - Tests should be independent and repeatable
## How to Run Tests
### In Xcode (Recommended)
1. Open `iosApp.xcodeproj`
2. Select HoneyDueUITests scheme
3. Press Cmd+U or click diamond icon next to test
### Command Line
```bash
xcodebuild test -project iosApp.xcodeproj -scheme HoneyDueUITests \
-destination 'platform=iOS Simulator,name=iPhone 17'
```
## Known Issues
- Previous comprehensive tests (Comprehensive*Tests.swift) are NOT working
- AccessibilityIdentifiers may not all be properly set in views
- Need to verify actual UI structure before writing complex tests
## Next Steps
1. ✅ Get SimpleLoginTest passing
2. Add more login/auth tests based on what works
3. Gradually add residence and task tests
4. Delete old failing comprehensive tests once new ones work

View File

@@ -1,198 +0,0 @@
# XCUITest Debugging Guide
## Current Status
**Completed:**
- Created comprehensive XCUITest infrastructure (34 tests)
- Added `AccessibilityIdentifiers.swift` with centralized identifiers
- Added accessibility identifiers to critical views (LoginView, RegisterView, MainTabView, Residence views, ProfileView)
- Updated `TestHelpers.swift` to use accessibility identifiers
- Project builds successfully for testing
**Issue:**
- ALL tests are failing within ~0.5 seconds
- Tests fail during `setUp()` when trying to login
- The login helper cannot find UI elements by their accessibility identifiers
## Root Cause Analysis
The tests fail at this line in `TestHelpers.swift:44`:
```swift
let usernameField = app.textFields[AccessibilityIdentifiers.Authentication.usernameField]
XCTAssertTrue(usernameField.waitForExistence(timeout: 5), "Username field should exist")
// This assertion fails - field not found
```
**Possible Causes:**
1. **Accessibility identifiers not set at runtime** - The `.accessibilityIdentifier()` modifiers might not be working
2. **App not rendering properly in test mode** - The app might be crashing or showing a different screen
3. **Timing issue** - The login screen might not be fully loaded when the test runs
4. **Kotlin initialization blocking** - The TokenStorage/Kotlin framework might be blocking UI rendering
## Recommended Next Steps
### Option 1: Debug in Xcode (RECOMMENDED)
This is the fastest way to diagnose the issue:
1. **Open the project in Xcode:**
```bash
cd /Users/treyt/Desktop/code/HoneyDue/HoneyDueKMM/iosApp
open iosApp.xcodeproj
```
2. **Select the Test target and a simulator:**
- Select "iPhone 17 Pro" simulator from the device dropdown
- Select the `HoneyDueTests` scheme
3. **Use UI Recording to see what elements exist:**
- Open `DebugLoginTest.swift`
- Place your cursor in `testAppLaunches()` method
- Click the **red record button** at the bottom of the editor
- The app will launch in the simulator
- **Tap on UI elements** - Xcode will generate code showing the actual identifiers
- Stop recording and examine the generated code
4. **Check console output:**
- Run `testAppLaunches()` test (Cmd+U or click diamond icon in gutter)
- View console output (Cmd+Shift+Y) to see the `XCTContext` activity logs
- This will show actual counts of UI elements found
5. **Use Accessibility Inspector:**
- Open Accessibility Inspector (Xcode → Open Developer Tool → Accessibility Inspector)
- Run the app normally (not in test mode)
- Inspect the login fields to verify accessibility identifiers are set
### Option 2: Add Debug Output to App
Modify `LoginView.swift` to print when it renders:
```swift
var body: some View {
// ... existing code ...
.onAppear {
print("🟢 LoginView appeared")
print("🔍 Username field identifier: \(AccessibilityIdentifiers.Authentication.usernameField)")
}
}
```
Then run tests and check if "LoginView appeared" prints in console.
### Option 3: Simplify the Test
The `DebugLoginTest.swift` is already simplified. Try running it:
```bash
cd /Users/treyt/Desktop/code/HoneyDue/HoneyDueKMM/iosApp
xcodebuild test \
-project iosApp.xcodeproj \
-scheme iosApp \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro' \
-only-testing:HoneyDueTests/DebugLoginTest/testAppLaunches
```
Check if it passes (meaning the app launches and has SOME UI elements).
## Known Issues to Check
### 1. AccessibilityIdentifiers Not in Test Target
Verify that `AccessibilityIdentifiers.swift` is included in the **iosApp target** (not HoneyDueTests):
- In Xcode, select `Helpers/AccessibilityIdentifiers.swift`
- In File Inspector (right panel), check "Target Membership"
- ✅ `iosApp` should be checked
- ❌ `HoneyDueTests` should NOT be checked
### 2. LoginView Not Using Correct Identifiers
Double-check `Login/LoginView.swift`:
```bash
grep "accessibilityIdentifier" /Users/treyt/Desktop/code/HoneyDue/HoneyDueKMM/iosApp/iosApp/Login/LoginView.swift
```
Should output:
```
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.usernameField)
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.passwordField)
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.loginButton)
```
### 3. App Showing Different Screen in Test Mode
The app might be checking for existing auth token and bypassing login. Check if `TokenStorage` has a stored token from previous runs:
```swift
// In iOSApp.swift init(), add for testing:
#if DEBUG
if ProcessInfo.processInfo.arguments.contains("--uitesting") {
TokenStorage.shared.clearToken() // Force logout for tests
}
#endif
```
### 4. Kotlin Framework Initialization Blocking
The app initializes `TokenStorage` in `iOSApp.init()`. This might be blocking:
```swift
init() {
// This could be blocking UI rendering:
TokenStorage.shared.initialize(manager: TokenManager())
}
```
Try moving initialization to background thread or making it async.
## Quick Verification Commands
```bash
# Check if identifiers are in LoginView
grep -c "accessibilityIdentifier" /Users/treyt/Desktop/code/HoneyDue/HoneyDueKMM/iosApp/iosApp/Login/LoginView.swift
# Should output: 6
# Check if AccessibilityIdentifiers exists
ls -la /Users/treyt/Desktop/code/HoneyDue/HoneyDueKMM/iosApp/iosApp/Helpers/AccessibilityIdentifiers.swift
# Should show the file
# Run simplified debug test
cd /Users/treyt/Desktop/code/HoneyDue/HoneyDueKMM/iosApp
xcodebuild test -project iosApp.xcodeproj -scheme iosApp \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro' \
-only-testing:HoneyDueTests/DebugLoginTest/testAppLaunches 2>&1 | grep "Test Case"
```
## Expected Output When Working
When tests work properly, you should see:
```
Test Case '-[HoneyDueTests.DebugLoginTest testAppLaunches]' started.
Activity 'Found 1 text fields' started
Activity 'Found 1 secure fields' started
Activity 'Found 5 buttons' started
Activity 'Email field exists: true' started
Activity 'Password field exists: true' started
Test Case '-[HoneyDueTests.DebugLoginTest testAppLaunches]' passed (5.234 seconds).
```
Currently seeing:
```
Test Case '-[HoneyDueTests.DebugLoginTest testAppLaunches]' failed (0.540 seconds)
```
The ~0.5 second failure suggests the app isn't even launching or is crashing immediately.
## Files Modified
- ✅ `/Users/treyt/Desktop/code/HoneyDue/HoneyDueKMM/iosApp/iosApp/Helpers/AccessibilityIdentifiers.swift` - Created
- ✅ `/Users/treyt/Desktop/code/HoneyDue/HoneyDueKMM/iosApp/iosApp/Login/LoginView.swift` - Added 6 identifiers
- ✅ `/Users/treyt/Desktop/code/HoneyDue/HoneyDueKMM/iosApp/iosApp/Login/RegisterView.swift` - Added 6 identifiers
- ✅ `/Users/treyt/Desktop/code/HoneyDue/HoneyDueKMM/iosApp/iosApp/MainTabView.swift` - Added 5 tab identifiers
- ✅ `/Users/treyt/Desktop/code/HoneyDue/HoneyDueKMM/iosApp/iosApp/Residence/*` - Added 15+ identifiers
- ✅ `/Users/treyt/Desktop/code/HoneyDue/HoneyDueKMM/iosApp/iosApp/Profile/ProfileTabView.swift` - Added logout identifier
- ✅ `/Users/treyt/Desktop/code/HoneyDue/HoneyDueKMM/iosApp/HoneyDueTests/TestHelpers.swift` - Updated to use identifiers
- ✅ `/Users/treyt/Desktop/code/HoneyDue/HoneyDueKMM/iosApp/HoneyDueTests/DebugLoginTest.swift` - Simplified debug test
## Next Action
**Open the project in Xcode** and use the UI Recording feature. This will immediately show you what identifiers are actually available and why the tests can't find them.

View File

@@ -1,599 +0,0 @@
# XCUITest Implementation Guide
## Overview
This guide provides step-by-step instructions for implementing comprehensive UI testing for the HoneyDue iOS app using XCUITest.
## Table of Contents
1. [Project Setup](#project-setup)
2. [Adding Accessibility Identifiers](#adding-accessibility-identifiers)
3. [Running the Tests](#running-the-tests)
4. [Continuous Integration](#continuous-integration)
---
## Project Setup
### Current Status
**Already Done:**
- UI Test target exists: `HoneyDueTests`
- Base test infrastructure in place (`TestHelpers.swift`, `BaseUITest`)
- Initial test files created
### What's New:
1. **Centralized Accessibility Identifiers**: `iosApp/Helpers/AccessibilityIdentifiers.swift`
2. **Comprehensive Test Suite**: Based on `AUTOMATED_TEST_EXECUTION_PLAN.md`
3. **Enhanced Test Utilities**: Improved helpers for common operations
---
## Adding Accessibility Identifiers
### Step 1: Import the Identifiers File
Ensure all view files import the identifiers:
```swift
import SwiftUI
// No import needed - same module
```
### Step 2: Add Identifiers to Views
For each interactive element in your views, add the `.accessibilityIdentifier()` modifier.
#### Example: LoginView.swift
```swift
// Username Field
TextField("Enter your email", text: $viewModel.username)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.keyboardType(.emailAddress)
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.usernameField)
// Password Field
SecureField("Enter your password", text: $viewModel.password)
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.passwordField)
// Login Button
Button(action: viewModel.login) {
Text("Sign In")
}
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.loginButton)
// Sign Up Button
Button("Sign Up") {
showingRegister = true
}
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.signUpButton)
```
#### Example: RegisterView.swift
```swift
TextField("Username", text: $viewModel.username)
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.registerUsernameField)
TextField("Email", text: $viewModel.email)
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.registerEmailField)
SecureField("Password", text: $viewModel.password)
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.registerPasswordField)
SecureField("Confirm Password", text: $viewModel.confirmPassword)
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.registerConfirmPasswordField)
Button("Register") {
viewModel.register()
}
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.registerButton)
```
#### Example: MainTabView.swift
```swift
TabView(selection: $selectedTab) {
NavigationView {
ResidencesListView()
}
.tabItem {
Label("Residences", systemImage: "house.fill")
}
.tag(0)
.accessibilityIdentifier(AccessibilityIdentifiers.Navigation.residencesTab)
// Repeat for other tabs...
}
```
#### Example: ResidenceFormView.swift
```swift
TextField("Property Name", text: $name)
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.nameField)
Picker("Property Type", selection: $selectedPropertyType) {
// ...
}
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.propertyTypePicker)
TextField("Street Address", text: $streetAddress)
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.streetAddressField)
TextField("City", text: $city)
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.cityField)
TextField("State/Province", text: $stateProvince)
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.stateProvinceField)
TextField("Postal Code", text: $postalCode)
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.postalCodeField)
TextField("Country", text: $country)
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.countryField)
TextField("Bedrooms", text: $bedrooms)
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.bedroomsField)
TextField("Bathrooms", text: $bathrooms)
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.bathroomsField)
Toggle("Primary Residence", isOn: $isPrimary)
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.isPrimaryToggle)
Button("Save") {
saveResidence()
}
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.saveButton)
```
#### Example: TaskFormView.swift
```swift
TextField("Title", text: $title)
.accessibilityIdentifier(AccessibilityIdentifiers.Task.titleField)
TextField("Description", text: $description)
.accessibilityIdentifier(AccessibilityIdentifiers.Task.descriptionField)
Picker("Category", selection: $selectedCategory) {
// ...
}
.accessibilityIdentifier(AccessibilityIdentifiers.Task.categoryPicker)
Picker("Frequency", selection: $selectedFrequency) {
// ...
}
.accessibilityIdentifier(AccessibilityIdentifiers.Task.frequencyPicker)
Picker("Priority", selection: $selectedPriority) {
// ...
}
.accessibilityIdentifier(AccessibilityIdentifiers.Task.priorityPicker)
DatePicker("Due Date", selection: $dueDate)
.accessibilityIdentifier(AccessibilityIdentifiers.Task.dueDatePicker)
TextField("Estimated Cost", text: $estimatedCost)
.accessibilityIdentifier(AccessibilityIdentifiers.Task.estimatedCostField)
Button("Save") {
saveTask()
}
.accessibilityIdentifier(AccessibilityIdentifiers.Task.saveButton)
```
#### Example: ResidenceDetailView.swift
```swift
// Add Task Button (FAB or toolbar button)
Button(action: { showingAddTask = true }) {
Image(systemName: "plus")
}
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.addTaskButton)
// Edit Button
Button("Edit") {
showingEditForm = true
}
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.editButton)
// Delete Button
Button("Delete", role: .destructive) {
showingDeleteConfirmation = true
}
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.deleteButton)
```
### Step 3: Dynamic Identifiers for List Items
For list items (residence cards, task cards, etc.), use dynamic identifiers:
```swift
ForEach(residences, id: \.id) { residence in
ResidenceCard(residence: residence)
.accessibilityIdentifier("\(AccessibilityIdentifiers.Residence.residenceCard).\(residence.id)")
.onTapGesture {
selectedResidence = residence
}
}
```
### Quick Reference: Files to Update
Here's a checklist of all views that need accessibility identifiers:
**Authentication (Priority: High)**
-`/iosApp/Login/LoginView.swift` - Partially done
-`/iosApp/Register/RegisterView.swift`
-`/iosApp/VerifyEmail/VerifyEmailView.swift`
-`/iosApp/PasswordReset/ForgotPasswordView.swift`
**Navigation (Priority: High)**
-`/iosApp/MainTabView.swift`
-`/iosApp/ContentView.swift`
**Residence (Priority: High)**
-`/iosApp/ResidenceFormView.swift`
-`/iosApp/Residence/ResidencesListView.swift`
-`/iosApp/Residence/ResidenceDetailView.swift`
-`/iosApp/AddResidenceView.swift`
-`/iosApp/EditResidenceView.swift`
**Task (Priority: High)**
-`/iosApp/Task/TaskFormView.swift`
-`/iosApp/Task/AddTaskView.swift`
-`/iosApp/Task/EditTaskView.swift`
-`/iosApp/Task/AllTasksView.swift`
-`/iosApp/Task/CompleteTaskView.swift`
**Contractor (Priority: Medium)**
-`/iosApp/Contractor/ContractorsListView.swift`
-`/iosApp/Contractor/ContractorDetailView.swift`
**Document (Priority: Medium)**
-`/iosApp/Documents/DocumentsWarrantiesView.swift`
-`/iosApp/Documents/AddDocumentView.swift`
-`/iosApp/Documents/DocumentDetailView.swift`
**Profile (Priority: Medium)**
-`/iosApp/Profile/ProfileView.swift`
-`/iosApp/Profile/ProfileTabView.swift`
---
## Running the Tests
### In Xcode
1. **Open the project:**
```bash
cd /Users/treyt/Desktop/code/HoneyDue/HoneyDueKMM/iosApp
open iosApp.xcodeproj
```
2. **Select the test target:**
- Product → Scheme → HoneyDueTests
3. **Choose a simulator:**
- iPhone 15 Pro (recommended)
- iOS 17.0+
4. **Run all tests:**
- Press `⌘ + U` (Command + U)
- Or: Product → Test
5. **Run specific test:**
- Open test file (e.g., `AuthenticationUITests.swift`)
- Click the diamond icon next to the test method
- Or: Right-click → Run Test
### From Command Line
```bash
# Navigate to iOS app directory
cd /Users/treyt/Desktop/code/HoneyDue/HoneyDueKMM/iosApp
# Run all tests
xcodebuild test \
-project iosApp.xcodeproj \
-scheme HoneyDueTests \
-destination 'platform=iOS Simulator,name=iPhone 15 Pro,OS=17.0'
# Run specific test class
xcodebuild test \
-project iosApp.xcodeproj \
-scheme HoneyDueTests \
-destination 'platform=iOS Simulator,name=iPhone 15 Pro,OS=17.0' \
-only-testing:HoneyDueTests/AuthenticationUITests
# Run specific test method
xcodebuild test \
-project iosApp.xcodeproj \
-scheme HoneyDueTests \
-destination 'platform=iOS Simulator,name=iPhone 15 Pro,OS=17.0' \
-only-testing:HoneyDueTests/AuthenticationUITests/testLoginWithValidCredentials
```
### Test Results
Test results are saved to:
```
~/Library/Developer/Xcode/DerivedData/iosApp-*/Logs/Test/
```
---
## Continuous Integration
### GitHub Actions Example
Create `.github/workflows/ios-tests.yml`:
```yaml
name: iOS UI Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
test:
runs-on: macos-14 # macOS Sonoma with Xcode 15+
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '15.2'
- name: Start Django Backend
run: |
cd honeyDueAPI
docker-compose up -d
sleep 10 # Wait for backend to start
- name: Check Backend Health
run: |
curl --retry 5 --retry-delay 3 http://localhost:8000/api/
- name: Run iOS UI Tests
run: |
cd HoneyDueKMM/iosApp
xcodebuild test \
-project iosApp.xcodeproj \
-scheme HoneyDueTests \
-destination 'platform=iOS Simulator,name=iPhone 15 Pro,OS=17.2' \
-resultBundlePath TestResults.xcresult \
-enableCodeCoverage YES
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: HoneyDueKMM/iosApp/TestResults.xcresult
- name: Upload Screenshots
if: failure()
uses: actions/upload-artifact@v4
with:
name: screenshots
path: ~/Library/Developer/CoreSimulator/Devices/*/data/tmp/
if-no-files-found: ignore
- name: Stop Docker Containers
if: always()
run: |
cd honeyDueAPI
docker-compose down
```
---
##Test Configuration
### Test Plan (Optional)
Create a `.xctestplan` file for better organization:
1. In Xcode: File → New → Test Plan
2. Name it: `HoneyDueTestPlan.xctestplan`
3. Configure:
- **Configurations**: Debug, Release
- **Test Targets**: HoneyDueTests
- **Code Coverage**: Enable
- **Screenshots**: Automatically on failure
### Launch Arguments
Configure launch arguments for testing:
```swift
// In test setUp()
app.launchArguments = [
"--uitesting", // Flag for UI testing mode
"--disable-animations", // Speed up tests
"--reset-user-defaults", // Clean state
"--use-test-api" // Point to test backend
]
app.launch()
```
Handle in your app:
```swift
// In AppDelegate or App init
if ProcessInfo.processInfo.arguments.contains("--uitesting") {
// Disable animations
UIView.setAnimationsEnabled(false)
// Clear user defaults
if ProcessInfo.processInfo.arguments.contains("--reset-user-defaults") {
let domain = Bundle.main.bundleIdentifier!
UserDefaults.standard.removePersistentDomain(forName: domain)
}
// Use test API
if ProcessInfo.processInfo.arguments.contains("--use-test-api") {
ApiConfig.CURRENT_ENV = .LOCAL
}
}
```
---
## Best Practices
### 1. Write Maintainable Tests
```swift
// ✅ Good: Descriptive test names
func testUserCanLoginWithValidCredentials() { }
// ❌ Bad: Vague test names
func testLogin() { }
```
### 2. Use Page Object Pattern
```swift
// Create a LoginPage helper
struct LoginPage {
let app: XCUIApplication
var usernameField: XCUIElement {
app.textFields[AccessibilityIdentifiers.Authentication.usernameField]
}
var passwordField: XCUIElement {
app.secureTextFields[AccessibilityIdentifiers.Authentication.passwordField]
}
var loginButton: XCUIElement {
app.buttons[AccessibilityIdentifiers.Authentication.loginButton]
}
func login(username: String, password: String) {
usernameField.tap()
usernameField.typeText(username)
passwordField.tap()
passwordField.typeText(password)
loginButton.tap()
}
}
// Use in tests
func testLogin() {
let loginPage = LoginPage(app: app)
loginPage.login(username: "testuser", password: "password")
// Assert...
}
```
### 3. Wait for Elements
```swift
// ✅ Good: Wait with timeout
let loginButton = app.buttons["Login"]
XCTAssertTrue(loginButton.waitForExistence(timeout: 5))
// ❌ Bad: Assume immediate availability
XCTAssertTrue(app.buttons["Login"].exists)
```
### 4. Clean State Between Tests
```swift
override func tearDown() {
// Logout if logged in
if app.tabBars.exists {
logout()
}
super.tearDown()
}
```
### 5. Use Meaningful Assertions
```swift
// ✅ Good: Clear assertion messages
XCTAssertTrue(
app.tabBars.exists,
"Should navigate to main tab view after login"
)
// ❌ Bad: No context
XCTAssertTrue(app.tabBars.exists)
```
---
## Troubleshooting
### Common Issues
**Issue: Elements not found**
- **Solution**: Verify accessibility identifier is added to the view
- **Debug**: Use `app.debugDescription` to see all elements
**Issue: Tests are flaky**
- **Solution**: Add waits (`waitForExistence`) before interactions
- **Solution**: Disable animations
**Issue: Tests run slow**
- **Solution**: Use `--disable-animations` launch argument
- **Solution**: Run tests in parallel (Xcode 13+)
**Issue: Backend not ready**
- **Solution**: Add health check before tests
- **Solution**: Increase wait time in pre-flight checks
### Debugging Tips
```swift
// Print all elements
print(app.debugDescription)
// Take screenshot manually
let screenshot = XCUIScreen.main.screenshot()
let attachment = XCTAttachment(screenshot: screenshot)
attachment.lifetime = .keepAlways
add(attachment)
// Breakpoint in test
// Use Xcode's test navigator to pause at failures
```
---
## Next Steps
1. ✅ Add accessibility identifiers to all views (see checklist above)
2. ✅ Run existing tests to verify setup
3. ✅ Review and expand test coverage using the new comprehensive test suite
4. ⬜ Set up CI/CD pipeline
5. ⬜ Configure test reporting/dashboards
---
## Resources
- [XCUITest Documentation](https://developer.apple.com/documentation/xctest/user_interface_tests)
- [WWDC: UI Testing in Xcode](https://developer.apple.com/videos/play/wwdc2019/413/)
- [Test Plan Configuration](https://developer.apple.com/documentation/xcode/organizing-tests-to-improve-feedback)
- [Accessibility for UIKit](https://developer.apple.com/documentation/uikit/accessibility)

View File

@@ -1,510 +0,0 @@
# XCUITest Implementation - Summary of Deliverables
## Overview
This document summarizes the comprehensive XCUITest implementation created for the HoneyDue iOS app. All deliverables are based on the `AUTOMATED_TEST_EXECUTION_PLAN.md` and follow iOS best practices for UI testing.
---
## What Was Delivered
### 1. ✅ Centralized Accessibility Identifiers
**File:** `iosApp/Helpers/AccessibilityIdentifiers.swift`
A comprehensive, centralized file containing all accessibility identifiers organized by feature:
- **Authentication**: Login, Registration, Verification, Password Reset
- **Navigation**: Tab bar items
- **Residence**: Forms, Lists, Details, Actions
- **Task**: Forms, Lists, Kanban, Completion
- **Contractor**: Forms, Lists, Details
- **Document**: Forms, Lists, Details
- **Profile**: Settings, Logout
- **Common**: Loading, Errors, Search, Filters
**Benefits:**
- Single source of truth for all identifiers
- Easy to maintain and update
- Compile-time safety (typo prevention)
- Consistent naming convention
**Usage Example:**
```swift
TextField("Email", text: $email)
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.usernameField)
```
---
### 2. ✅ Comprehensive Test Suite
Three complete test files covering all major flows from the automated test plan:
####ComprehensiveAuthenticationTests.swift`
**Location:** `iosApp/HoneyDueTests/ComprehensiveAuthenticationTests.swift`
**Tests Included:**
- `testUserRegistrationComplete()` - Test 1.1: Full registration flow
- `testRegistrationWithExistingUsername()` - Validation test
- `testRegistrationWithInvalidEmail()` - Validation test
- `testRegistrationWithMismatchedPasswords()` - Validation test
- `testLoginWithValidCredentials()` - Test 1.3: Successful login
- `testLoginWithInvalidCredentials()` - Error handling
- `testLoginWithEmptyFields()` - Validation test
- `testPasswordVisibilityToggle()` - UI interaction test
- `testLogout()` - Test 1.2: Logout flow
- `testLogoutClearsSession()` - Session management test
- `testForgotPasswordFlow()` - Password reset navigation
**Coverage:**
- ✅ User registration with email verification
- ✅ Login with valid/invalid credentials
- ✅ Logout and session clearing
- ✅ Form validation
- ✅ Error message display
- ✅ UI interactions (password visibility)
---
#### `ComprehensiveResidenceTests.swift`
**Location:** `iosApp/HoneyDueTests/ComprehensiveResidenceTests.swift`
**Tests Included:**
- `testCreateResidenceComplete()` - Test 2.1: Create property
- `testCreateResidenceWithMinimalData()` - Minimal field test
- `testCreateResidenceValidation()` - Required field validation
- `testViewResidenceDetails()` - Test 2.2: View details
- `testResidenceDetailShowsAllInfo()` - Data display verification
- `testEditResidence()` - Test 2.3: Edit property
- `testEditResidenceCancel()` - Cancel edit test
- `testDeleteResidence()` - Delete confirmation flow
- `testDeleteResidenceCancellation()` - Cancel delete test
- `testEmptyStateDisplay()` - Empty state UI
**Coverage:**
- ✅ Create residence with full/minimal data
- ✅ View residence details
- ✅ Edit residence information
- ✅ Delete residence with confirmation
- ✅ Form validation
- ✅ Empty state handling
**Helper Methods:**
- `fillResidenceForm()` - Reusable form filling
- `ensureResidenceExists()` - Test data setup
- `countResidenceCards()` - List verification
- `scrollToFind()` - Dynamic element location
---
#### `ComprehensiveTaskTests.swift`
**Location:** `iosApp/HoneyDueTests/ComprehensiveTaskTests.swift`
**Tests Included:**
- `testCreateOneTimeTaskComplete()` - Test 3.1: Create one-time task
- `testCreateRecurringTask()` - Recurring task creation
- `testCreateTaskWithAllFields()` - Complete form test
- `testCreateTaskValidation()` - Required field validation
- `testMarkTaskInProgress()` - Test 3.2: Status change
- `testMarkTaskInProgressFromKanban()` - Kanban interaction
- `testCompleteTask()` - Test 3.3: Task completion
- `testCompleteTaskWithPhotos()` - Photo upload test
- `testCompleteTaskMinimal()` - Minimal completion test
- `testKanbanViewColumns()` - Kanban UI verification
- `testTaskFilteringByResidence()` - Filtering test
- `testEditTask()` - Edit task flow
- `testDeleteTask()` - Delete task flow
**Coverage:**
- ✅ Create one-time and recurring tasks
- ✅ Mark tasks as in progress
- ✅ Complete tasks with full/minimal data
- ✅ Kanban board navigation
- ✅ Task filtering
- ✅ Edit and delete tasks
**Helper Methods:**
- `fillTaskForm()` - Reusable task creation
- `fillCompletionForm()` - Completion form filling
- `ensureTaskExists()` - Test data setup
- `navigateToTaskDetail()` - Navigation helper
- `selectPicker()` - Picker interaction
---
### 3. ✅ Implementation Guide
**File:** `iosApp/XCUITEST_IMPLEMENTATION_GUIDE.md`
A comprehensive 400+ line guide covering:
**Section 1: Project Setup**
- Current infrastructure status
- What's already done vs. what's new
**Section 2: Adding Accessibility Identifiers**
- Step-by-step instructions with code examples
- Examples for all view types (LoginView, RegisterView, Forms, Lists, etc.)
- Dynamic identifier patterns for list items
- Complete checklist of 30+ files to update
**Section 3: Running the Tests**
- Running in Xcode (GUI)
- Running from command line
- Test result locations
- Filtering and targeting specific tests
**Section 4: Continuous Integration**
- Complete GitHub Actions workflow example
- Backend startup integration
- Test result artifact upload
- Screenshot capture on failure
**Section 5: Test Configuration**
- Test plan creation
- Launch arguments for testing mode
- Disabling animations for faster tests
- Using test API endpoints
**Section 6: Best Practices**
- Descriptive test names
- Page Object Pattern examples
- Waiting for elements properly
- Clean state between tests
- Meaningful assertions
**Section 7: Troubleshooting**
- Common issues and solutions
- Debugging tips
- Element not found scenarios
- Flaky test fixes
**Section 8: Resources**
- Apple documentation links
- WWDC session references
- Best practice guides
---
### 4. ✅ Enhanced Test Helpers (Already Existing)
**File:** `iosApp/HoneyDueTests/TestHelpers.swift` (Already in project)
The existing test infrastructure includes:
- `BaseUITest` class with setup/teardown
- Authentication helpers (`login()`, `logout()`, `register()`)
- Navigation helpers (`navigateToTab()`, `navigateBack()`)
- Assertion helpers (`assertElementExists()`, `assertNavigatedTo()`)
- Wait helpers (`wait()`, `waitForElementToAppear()`)
- Identifiers struct (now superseded by centralized file)
**New additions recommended:**
- `scrollToElement()` - Scroll to make element visible
- `clearTextField()` - Clear text field content
- `fillForm()` - Generic form filling helper
- `takeScreenshot()` - Manual screenshot capture
- `verifyBackendState()` - Django shell verification
---
## Implementation Roadmap
###Phase 1: Add Accessibility Identifiers (Highest Priority)
**Estimated Time:** 2-4 hours
Follow the checklist in `XCUITEST_IMPLEMENTATION_GUIDE.md`:
1. **Authentication Views** (30 min)
-`LoginView.swift` - Partially complete
-`RegisterView.swift`
-`VerifyEmailView.swift`
-`ForgotPasswordView.swift`
2. **Navigation Views** (15 min)
-`MainTabView.swift`
-`ContentView.swift`
3. **Residence Views** (45 min)
-`ResidenceFormView.swift`
-`ResidencesListView.swift`
-`ResidenceDetailView.swift`
-`AddResidenceView.swift`
-`EditResidenceView.swift`
4. **Task Views** (45 min)
-`TaskFormView.swift`
-`AddTaskView.swift`
-`EditTaskView.swift`
-`AllTasksView.swift`
-`CompleteTaskView.swift`
5. **Other Views** (1 hour)
- ⬜ Contractor views
- ⬜ Document views
- ⬜ Profile views
**How to Add:**
For each view file, add `.accessibilityIdentifier()` to interactive elements:
```swift
// Before
TextField("Email", text: $email)
// After
TextField("Email", text: $email)
.accessibilityIdentifier(AccessibilityIdentifiers.Authentication.registerEmailField)
```
---
### Phase 2: Run and Verify Tests (30 min)
1. **Open Xcode**
```bash
cd /Users/treyt/Desktop/code/HoneyDue/HoneyDueKMM/iosApp
open iosApp.xcodeproj
```
2. **Select Test Target**
- Product → Scheme → HoneyDueTests
3. **Run Individual Test**
- Open `ComprehensiveAuthenticationTests.swift`
- Click diamond next to `testLoginWithValidCredentials()`
- Verify it passes
4. **Run Full Suite**
- Press `⌘ + U` to run all tests
- Review results in Test Navigator
5. **Fix Failing Tests**
- Update identifiers if elements not found
- Adjust waits if timing issues
- Check backend is running
---
### Phase 3: Integrate with CI/CD (Optional, 1 hour)
1. **Create GitHub Actions workflow**
- Copy example from implementation guide
- Save as `.github/workflows/ios-tests.yml`
2. **Configure secrets**
- Add any required API keys
- Configure backend URL
3. **Test locally**
- Run workflow locally with `act` tool
- Verify all steps work
4. **Push and monitor**
- Commit workflow file
- Monitor first run
- Configure notifications
---
## Test Coverage Summary
### Current Coverage (After Implementation)
| Feature Area | Test Count | Status |
|-------------|-----------|--------|
| Authentication | 11 tests | ✅ Complete |
| Residence Management | 10 tests | ✅ Complete |
| Task Management | 13 tests | ✅ Complete |
| **Total** | **34 tests** | **Ready to Run** |
### Areas Not Yet Covered (Future Work)
- Contractor management (5-7 tests)
- Document/Warranty management (5-7 tests)
- Multi-user/sharing features (4-6 tests)
- Profile settings (3-5 tests)
- Pull-to-refresh (1 test)
- Search/filtering (2-3 tests)
- Performance tests (3-5 tests)
**Estimated Total Possible Coverage:** 60-70 comprehensive tests
---
## Key Benefits
### 1. **Regression Prevention**
- Catch breaking changes before production
- Verify critical flows work after updates
- Confident refactoring
### 2. **Documentation**
- Tests serve as living documentation
- Show how features should work
- Onboard new developers faster
### 3. **Faster Development**
- No manual testing for every change
- Quick feedback on bugs
- Automated smoke testing
### 4. **Quality Assurance**
- Consistent test execution
- No human error in testing
- Comprehensive coverage
### 5. **CI/CD Integration**
- Automated testing on every PR
- Block merges if tests fail
- Continuous quality monitoring
---
## Next Steps
### Immediate (This Week)
1. ✅ Add accessibility identifiers to authentication views
2. ✅ Run authentication tests to verify setup
3. ✅ Add identifiers to residence views
4. ✅ Run residence tests
### Short Term (This Month)
1. ⬜ Add identifiers to all remaining views
2. ⬜ Run full test suite
3. ⬜ Fix any failing tests
4. ⬜ Add contractor and document tests
5. ⬜ Set up CI/CD pipeline
### Long Term (Next Quarter)
1. ⬜ Achieve 80%+ UI coverage
2. ⬜ Add performance benchmarks
3. ⬜ Implement visual regression tests
4. ⬜ Create test data factories
5. ⬜ Add accessibility audit tests
---
## Files Created/Modified
### New Files Created
1. **`iosApp/Helpers/AccessibilityIdentifiers.swift`** (253 lines)
- Centralized accessibility identifiers
2. **`iosApp/HoneyDueTests/ComprehensiveAuthenticationTests.swift`** (232 lines)
- 11 authentication tests
3. **`iosApp/HoneyDueTests/ComprehensiveResidenceTests.swift`** (387 lines)
- 10 residence management tests
4. **`iosApp/HoneyDueTests/ComprehensiveTaskTests.swift`** (437 lines)
- 13 task management tests
5. **`iosApp/XCUITEST_IMPLEMENTATION_GUIDE.md`** (451 lines)
- Complete implementation guide
6. **`iosApp/XCUITEST_IMPLEMENTATION_SUMMARY.md`** (This file)
- Summary and roadmap
**Total New Code:** ~1,760 lines of production-ready code and documentation
### Files To Be Modified
1. **LoginView.swift** - Add 6 more identifiers
2. **RegisterView.swift** - Add 6 identifiers
3. **MainTabView.swift** - Add 5 identifiers
4. **ResidenceFormView.swift** - Add 15 identifiers
5. **TaskFormView.swift** - Add 12 identifiers
6. **~25 more view files** - Add identifiers as needed
---
## Maintenance Guide
### When Adding New Features
1. **Add accessibility identifier constants**
```swift
// In AccessibilityIdentifiers.swift
struct NewFeature {
static let actionButton = "NewFeature.ActionButton"
static let inputField = "NewFeature.InputField"
}
```
2. **Add identifier to view**
```swift
Button("Action") { }
.accessibilityIdentifier(AccessibilityIdentifiers.NewFeature.actionButton)
```
3. **Write test**
```swift
func testNewFeature() {
let button = app.buttons[AccessibilityIdentifiers.NewFeature.actionButton]
button.tap()
// Assert...
}
```
### When Modifying UI
1. **Check if identifier exists**
- Search `AccessibilityIdentifiers.swift`
2. **Update if needed**
- Rename identifier if element purpose changed
- Update all test references
3. **Run affected tests**
- Ensure tests still pass
- Update assertions if behavior changed
---
## Support and Resources
### Documentation
- `XCUITEST_IMPLEMENTATION_GUIDE.md` - Complete how-to guide
- `AUTOMATED_TEST_EXECUTION_PLAN.md` - Original test plan
- `TestHelpers.swift` - Existing helper methods
### Apple Resources
- [XCUITest Documentation](https://developer.apple.com/documentation/xctest/user_interface_tests)
- [Accessibility for UIKit](https://developer.apple.com/documentation/uikit/accessibility)
- [Writing Testable Code](https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods)
### Contact
For questions about this implementation, refer to the guide or check:
- Test file comments
- Inline code documentation
- Apple's official XCUITest documentation
---
## Summary
This implementation provides a **complete, production-ready XCUITest infrastructure** for the HoneyDue iOS app:
**34 comprehensive tests** covering authentication, residences, and tasks
**Centralized accessibility identifiers** for maintainability
**Complete implementation guide** with examples and best practices
**CI/CD integration examples** for automation
**Helper methods** for test data setup and navigation
**Clear roadmap** for completing implementation
**Estimated time to complete:** 3-5 hours to add all accessibility identifiers and run full suite.
**Ready to run:** Yes! Once accessibility identifiers are added to views, tests can be executed immediately.
---
**Last Updated:** November 18, 2025
**Version:** 1.0
**Author:** Claude Code

View File

@@ -1,24 +0,0 @@
# XCUITest Authoring
## Required Architecture
- Put shared test infrastructure in `/Users/treyt/Desktop/code/HoneyDueKMM/iosApp/HoneyDueUITests/Framework`.
- Put feature suites in `/Users/treyt/Desktop/code/HoneyDueKMM/iosApp/HoneyDueUITests/Tests`.
- Every test suite inherits `BaseUITestCase`.
- Reusable multi-step setup belongs in `TestFlows`.
- UI interactions should go through screen objects in `ScreenObjects.swift`.
## Runtime Contract
- Launch args are standardized in `BaseUITestCase`:
- `--ui-testing`
- `--disable-animations`
- `--reset-state`
- App-side behavior for UI test mode is implemented in `/Users/treyt/Desktop/code/HoneyDueKMM/iosApp/iosApp/Helpers/UITestRuntime.swift`.
## Naming
- Test method naming format: `test<CaseID>_<BehaviorDescription>()`.
- Case IDs should stay stable once committed.
## Waiting and Flake Rules
- Use helper waits from `BaseUITestCase` extensions.
- Do not add blind `sleep()`.
- Prefer stable accessibility identifiers over visible text selectors.

View File

@@ -1,383 +0,0 @@
# iOS Shared Utilities Migration Summary
## Overview
Successfully migrated the iOS codebase at `/Users/treyt/Desktop/code/HoneyDue/HoneyDueKMM/iosApp/iosApp/` to use shared utilities from `/Users/treyt/Desktop/code/HoneyDue/HoneyDueKMM/iosApp/iosApp/Shared/`.
## Migration Date
December 17, 2025
## Key Changes Made
### 1. Form Styling Standardization
**Pattern Replaced:**
```swift
// OLD
.listStyle(.plain)
.scrollContentBackground(.hidden)
.background(Color.appBackground)
// or
.background(Color.clear)
// NEW
.standardFormStyle()
```
**Files Updated:**
- TaskFormView.swift
- CompleteTaskView.swift
- ThemeSelectionView.swift
- TaskTemplatesBrowserView.swift
- ContractorFormSheet.swift
- ProfileView.swift
- NotificationPreferencesView.swift
- DocumentFormView.swift
### 2. Section Background Standardization
**Pattern Replaced:**
```swift
// OLD
.listRowBackground(Color.appBackgroundSecondary)
// NEW
.sectionBackground()
```
**Files Updated:** All form-based views (20+ files)
### 3. Date Formatting
**Pattern Replaced:**
```swift
// OLD
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
let dateString = formatter.string(from: date)
// Or
DateUtils.formatDate(effectiveDate)
// NEW
date.formattedAPI()
effectiveDate.toFormattedDate()
```
**Extensions Used:**
- `.formatted()` - "MMM d, yyyy"
- `.formattedAPI()` - "yyyy-MM-dd"
- `.toFormattedDate()` - String to formatted date
- `.isOverdue()` - Check if date is overdue
**Files Updated:**
- TaskFormView.swift (date parsing and formatting)
- TaskCard.swift (date display)
- DynamicTaskCard.swift (date display)
- CompletionCardView.swift (date display)
### 4. Currency Formatting
**Pattern Replaced:**
```swift
// OLD
Text("Cost: $\(cost)")
// NEW
Text("Cost: \(cost.toCurrency())")
```
**Files Updated:**
- CompletionCardView.swift
### 5. Form Component Standardization
**Pattern Replaced:**
```swift
// OLD - Manual field with icon
VStack(alignment: .leading, spacing: 8) {
Text(label)
.font(.system(size: 14, weight: .medium, design: .rounded))
.foregroundColor(Color.appTextSecondary)
HStack(spacing: 12) {
ZStack {
Circle()
.fill(Color.appPrimary.opacity(0.1))
.frame(width: 32, height: 32)
Image(systemName: icon)
.font(.system(size: 14, weight: .medium))
.foregroundColor(Color.appPrimary)
}
TextField(placeholder, text: $text)
// ... many lines of styling
}
.padding(16)
.background(Color.appBackgroundPrimary.opacity(0.5))
// ... more styling
}
// NEW
VStack(alignment: .leading, spacing: 8) {
FieldLabel(text: label)
IconTextField(
icon: icon,
placeholder: placeholder,
text: $text,
keyboardType: .emailAddress,
onSubmit: { /* action */ }
)
}
```
**Files Updated:**
- LoginView.swift (username and password fields)
### 6. Button Standardization
**Pattern Replaced:**
```swift
// OLD - Manual button with loading state
Button(action: action) {
HStack(spacing: AppSpacing.sm) {
if isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
}
Text(isLoading ? "Loading..." : "Submit")
.font(.headline)
.fontWeight(.semibold)
}
.frame(maxWidth: .infinity)
.frame(height: 56)
.foregroundColor(Color.appTextOnPrimary)
.background(buttonBackground)
.cornerRadius(AppRadius.md)
.shadow(color: shouldShowShadow ? Color.appPrimary.opacity(0.3) : .clear, radius: 10, y: 5)
}
// NEW
OrganicPrimaryButton(
title: "Submit",
isLoading: isLoading,
isDisabled: !isValid,
action: action
)
```
**Files Updated:**
- LoginView.swift (login button)
### 7. Error Field Display
**Pattern Replaced:**
```swift
// OLD
if !error.isEmpty {
Text(error)
.font(.caption)
.foregroundColor(Color.appError)
}
// NEW
if !error.isEmpty {
FieldError(message: error)
}
```
**Files Updated:**
- TaskFormView.swift (title and residence errors)
### 8. Loading Overlay
**Pattern Replaced:**
```swift
// OLD
.disabled(isLoading)
.blur(radius: isLoading ? 3 : 0)
if isLoading {
VStack(spacing: OrganicSpacing.comfortable) {
ZStack {
Circle()
.fill(Color.appPrimary.opacity(0.1))
.frame(width: 64, height: 64)
ProgressView()
.scaleEffect(1.2)
.tint(Color.appPrimary)
}
Text("Loading...")
.font(.system(size: 15, weight: .medium, design: .rounded))
.foregroundColor(Color.appTextSecondary)
}
// ... 20+ more lines of styling
}
// NEW
.loadingOverlay(isLoading: isLoading, message: "Loading...")
```
**Files Updated:**
- TaskFormView.swift
### 9. Empty State Views
**Pattern Replaced:**
```swift
// OLD - Manual empty state
VStack(spacing: 16) {
ZStack {
Circle()
.fill(
RadialGradient(
colors: [
Color.appPrimary.opacity(0.12),
Color.appPrimary.opacity(0.04)
],
center: .center,
startRadius: 0,
endRadius: 50
)
)
.frame(width: 80, height: 80)
Image(systemName: icon)
.font(.system(size: 32, weight: .medium))
.foregroundColor(Color.appPrimary.opacity(0.6))
}
VStack(spacing: 8) {
Text(title)
.font(.system(size: 17, weight: .semibold, design: .rounded))
.foregroundColor(Color.appTextPrimary)
Text(subtitle)
.font(.system(size: 14, weight: .medium))
.foregroundColor(Color.appTextSecondary)
.multilineTextAlignment(.center)
}
}
.frame(maxWidth: .infinity)
.padding(OrganicSpacing.spacious)
.background(OrganicCardBackground(showBlob: true, blobVariation: 1))
.clipShape(RoundedRectangle(cornerRadius: 28, style: .continuous))
.naturalShadow(.subtle)
// NEW
OrganicEmptyState(
icon: icon,
title: title,
subtitle: subtitle,
blobVariation: 1
)
```
**Files Updated:**
- ResidenceDetailView.swift (contractors empty state)
### 10. Standard Loading View
**Pattern Replaced:**
```swift
// OLD
VStack(spacing: 16) {
ProgressView()
Text("Loading...")
.font(.subheadline)
.foregroundColor(Color.appTextSecondary)
}
// NEW
StandardLoadingView(message: "Loading...")
```
**Files Updated:**
- ResidenceDetailView.swift
## Benefits of Migration
### 1. Code Reduction
- **Estimated lines removed**: 500+ lines of duplicate code
- **Average reduction per file**: 20-50 lines
### 2. Consistency
- All forms now use consistent styling
- All date formatting follows the same pattern
- All empty states have consistent design
- All loading states look the same
### 3. Maintainability
- Changes to styling can be made in one place
- Easier to update design system
- Less duplication means fewer bugs
- Clearer intent in code
### 4. Performance
- Centralized DateFormatters are reused (better performance)
- Single source of truth for styling
## Files Modified (Summary)
### Core Views
- LoginView.swift
- TaskFormView.swift
- TaskCard.swift
- ResidenceDetailView.swift
- CompleteTaskView.swift
### Profile Views
- ProfileView.swift
- ProfileTabView.swift
- ThemeSelectionView.swift
- NotificationPreferencesView.swift
### Task Views
- TaskTemplatesBrowserView.swift
- DynamicTaskCard.swift
- CompletionCardView.swift
### Document & Contractor Views
- DocumentFormView.swift
- ContractorFormSheet.swift
### Total Files Modified: 15+
## Shared Utilities Used
### Extensions
- `ViewExtensions.swift` - Form styling, loading overlays, modifiers
- `DateExtensions.swift` - Date formatting and parsing
- `StringExtensions.swift` - String validation and utilities
- `DoubleExtensions.swift` - Currency formatting
### Components
- `FormComponents.swift` - IconTextField, FieldLabel, FieldError
- `SharedEmptyStateView.swift` - OrganicEmptyState, StandardLoadingView
- `ButtonStyles.swift` - OrganicPrimaryButton, SecondaryButton
### Utilities
- `ValidationHelpers.swift` - Form validation (available but not yet fully utilized)
- `SharedErrorMessageParser.swift` - Error parsing (already in use)
## Next Steps
1. ✅ All major views have been migrated
2. Consider migrating validation logic to use `ValidationHelpers`
3. Consider replacing more manual buttons with shared button components
4. Update any new code to follow these patterns
## Testing Recommendations
1. Test all forms to ensure styling is correct
2. Verify date formatting in all views
3. Test loading states and empty states
4. Ensure all buttons work as expected
5. Check that error messages display correctly
## Notes
- The migration maintains all existing functionality
- No behavior changes, only code organization improvements
- All changes are backwards compatible
- Original organic design aesthetic is preserved

View File

@@ -1,841 +0,0 @@
# iOS Code Refactoring Analysis - DRY Principles
## Executive Summary
This document summarizes the comprehensive analysis of the iOS codebase at `/Users/treyt/Desktop/code/HoneyDue/HoneyDueKMM/iosApp/iosApp/` and provides a complete set of shared utilities to eliminate code duplication.
## Status
**NEW SHARED UTILITIES CREATED** (All files compiled successfully):
All shared utilities have been created in the `/Shared` directory and are ready for use. The existing codebase has NOT been modified to avoid breaking changes.
## Files Created
### Extensions
- `/Shared/Extensions/ViewExtensions.swift` - Form styling, loading overlays, conditional modifiers
- `/Shared/Extensions/DateExtensions.swift` - Date formatting, conversions, and utilities
- `/Shared/Extensions/StringExtensions.swift` - String validation and utilities
- `/Shared/Extensions/DoubleExtensions.swift` - Number/currency formatting
### Components
- `/Shared/Components/FormComponents.swift` - Reusable form components
- `/Shared/Components/SharedEmptyStateView.swift` - Empty state components
- `/Shared/Components/ButtonStyles.swift` - Standardized button components
### Modifiers
- `/Shared/Modifiers/CardModifiers.swift` - Card styling modifiers
### Utilities
- `/Shared/Utilities/ValidationHelpers.swift` - Form validation utilities
- `/Shared/Utilities/SharedErrorMessageParser.swift` - Error message parsing
### Documentation
- `/Shared/SHARED_UTILITIES.md` - Complete usage guide
- `/Shared/REFACTORING_SUMMARY.md` - This file
## Identified Repeated Patterns
### 1. Form Styling Pattern (43 occurrences)
**Current Pattern:**
```swift
Form {
// content
}
.listStyle(.plain)
.scrollContentBackground(.hidden)
.background(Color.appBackgroundPrimary)
Section {
// fields
}
.listRowBackground(Color.appBackgroundSecondary)
```
**New Shared Pattern:**
```swift
Form {
Section {
// fields
}
.sectionBackground()
}
.standardFormStyle()
```
**Files to Refactor:**
- TaskFormView.swift
- ProfileView.swift
- LoginView.swift
- AddResidenceView.swift
- EditResidenceView.swift
- AddDocumentView.swift
- EditDocumentView.swift
- NotificationPreferencesView.swift
- ThemeSelectionView.swift
**Savings:** ~86 lines of code eliminated
---
### 2. Date Formatting Pattern (27 occurrences)
**Current Pattern:**
```swift
let formatter = DateFormatter()
formatter.dateFormat = "MMM d, yyyy"
let dateString = formatter.string(from: date)
// or
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
let date = formatter.date(from: string)
```
**New Shared Pattern:**
```swift
let dateString = date.formatted() // "Jan 15, 2024"
let apiDate = date.formattedAPI() // "2024-01-15"
let date = "2024-01-15".toDate()
let formatted = "2024-01-15".toFormattedDate()
```
**Files to Refactor:**
- TaskFormView.swift (lines 502-504)
- TaskCard.swift (line 58)
- CompletionHistorySheet.swift
- DocumentHelpers.swift
- Multiple ViewModels
**Savings:** ~81 lines of code eliminated, centralized formatters improve performance
---
### 3. Loading State Pattern (18 occurrences)
**Current Pattern:**
```swift
ZStack {
content
.disabled(isLoading)
.blur(radius: isLoading ? 3 : 0)
if isLoading {
VStack(spacing: OrganicSpacing.comfortable) {
ZStack {
Circle()
.fill(Color.appPrimary.opacity(0.1))
.frame(width: 64, height: 64)
ProgressView()
.scaleEffect(1.2)
.tint(Color.appPrimary)
}
Text("Loading...")
.font(.system(size: 15, weight: .medium, design: .rounded))
.foregroundColor(Color.appTextSecondary)
}
// ... 15 more lines
}
}
```
**New Shared Pattern:**
```swift
VStack {
// content
}
.loadingOverlay(isLoading: viewModel.isLoading, message: "Loading...")
```
**Files to Refactor:**
- TaskFormView.swift (lines 305-331)
- ProfileView.swift (lines 18-31)
- ResidenceDetailView.swift (lines 199-206)
- DocumentFormView.swift
**Savings:** ~252 lines of code eliminated
---
### 4. Number/Currency Formatting Pattern (15 occurrences)
**Current Pattern:**
```swift
// Converting Double to String
let costString = estimatedCost != nil ? String(estimatedCost!.doubleValue) : ""
// Converting String to Double
let cost = estimatedCost.isEmpty ? nil : Double(estimatedCost) ?? 0.0
// Format as currency
let formatter = NumberFormatter()
formatter.numberStyle = .currency
return formatter.string(from: NSNumber(value: cost)) ?? "$\(cost)"
// Format file size
var size = Double(bytes)
let units = ["B", "KB", "MB", "GB"]
var unitIndex = 0
while size >= 1024 && unitIndex < units.count - 1 {
size /= 1024
unitIndex += 1
}
return String(format: "%.1f %@", size, units[unitIndex])
```
**New Shared Pattern:**
```swift
// Currency formatting
let formatted = price.toCurrency() // "$1,234.56"
// File size formatting
let size = bytes.toFileSize() // "1.2 MB"
// Decimal formatting
let formatted = value.toDecimalString(fractionDigits: 2) // "1,234.56"
// Percentage
let formatted = value.toPercentage() // "45.5%"
```
**Files to Refactor:**
- DocumentCard.swift (lines 91-102)
- TaskFormView.swift (line 524, 561)
- ContractorFormView.swift
- ResidenceFormView.swift
**Savings:** ~60 lines of code eliminated, consistent formatting app-wide
---
### 5. Validation Pattern (22 occurrences)
**Current Pattern:**
```swift
if title.isEmpty {
titleError = "Title is required"
isValid = false
} else {
titleError = ""
}
if email.isEmpty {
return "Email is required"
} else if !email.contains("@") {
return "Invalid email"
}
if password.count < 8 {
return "Password must be at least 8 characters"
}
```
**New Shared Pattern:**
```swift
// Single field validation
let result = ValidationHelpers.validateEmail(email)
if case .invalid(let message) = result {
errorMessage = message
}
// Form validation
let validator = FormValidator()
validator.add(fieldName: "email") {
ValidationHelpers.validateEmail(email)
}
validator.add(fieldName: "password") {
ValidationHelpers.validatePassword(password)
}
let result = validator.validate()
if !result.isValid {
errorMessage = result.errors.values.first
}
```
**Files to Refactor:**
- TaskFormView.swift (lines 457-490)
- LoginViewModel.swift
- RegisterViewModel.swift
- ProfileViewModel.swift
- ContractorFormState.swift
- ResidenceFormState.swift
**Savings:** ~132 lines of code eliminated, consistent error messages
---
### 6. Card Styling Pattern (35 occurrences)
**Current Pattern:**
```swift
VStack {
// content
}
.padding(AppSpacing.md)
.background(Color.appBackgroundSecondary)
.cornerRadius(AppRadius.md)
.shadow(color: Color.black.opacity(0.05), radius: 2, x: 0, y: 1)
// or organic variant
VStack {
// content
}
.padding(OrganicSpacing.cozy)
.background(OrganicCardBackground(showBlob: true, blobVariation: 1))
.clipShape(RoundedRectangle(cornerRadius: 24, style: .continuous))
.naturalShadow(.medium)
```
**New Shared Pattern:**
```swift
VStack {
// content
}
.standardCard()
// or organic variant
VStack {
// content
}
.organicCardStyle()
// or list row
HStack {
// content
}
.listRowCard()
```
**Files to Refactor:**
- TaskCard.swift (lines 198-202)
- DocumentCard.swift (lines 81-84)
- ContractorCard.swift (lines 85-88)
- PropertyHeaderCard.swift
- TaskSummaryCard.swift
- ShareCodeCard.swift
**Savings:** ~140 lines of code eliminated
---
### 7. Empty State Pattern (12 occurrences)
**Current Pattern:**
```swift
if items.isEmpty {
VStack(spacing: 16) {
Image(systemName: "tray")
.font(.system(size: 60))
.foregroundColor(Color.appTextSecondary.opacity(0.5))
Text("No Items")
.font(.headline)
.foregroundColor(Color.appTextPrimary)
Text("Get started by adding your first item")
.font(.subheadline)
.foregroundColor(Color.appTextSecondary)
.multilineTextAlignment(.center)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
}
// or organic variant
VStack(spacing: 16) {
ZStack {
Circle()
.fill(RadialGradient(...))
.frame(width: 80, height: 80)
Image(systemName: icon)
.font(.system(size: 32, weight: .medium))
.foregroundColor(accentColor.opacity(0.6))
}
// ... 25 more lines
}
```
**New Shared Pattern:**
```swift
// Standard empty state
if items.isEmpty {
EmptyStateView(
icon: "tray",
title: "No Items",
subtitle: "Get started by adding your first item",
actionLabel: "Add Item",
action: { showAddForm = true }
)
}
// Organic empty state
OrganicEmptyState(
icon: "checkmark.circle",
title: "All Done!",
subtitle: "You have no pending tasks"
)
// List empty state
ListEmptyState(
icon: "tray",
message: "No items to display"
)
```
**Files to Refactor:**
- ResidenceDetailView.swift (lines 284-321)
- DocumentsTabContent.swift
- WarrantiesTabContent.swift
- TasksSection.swift
**Savings:** ~180 lines of code eliminated
---
### 8. Button Styling Pattern (28 occurrences)
**Current Pattern:**
```swift
Button(action: viewModel.login) {
HStack(spacing: 8) {
if viewModel.isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
}
Text(viewModel.isLoading ? "Signing In..." : "Log In")
.font(.headline)
.fontWeight(.semibold)
}
.frame(maxWidth: .infinity)
.frame(height: 56)
.foregroundColor(Color.appTextOnPrimary)
.background(
viewModel.isLoading || !isFormValid
? Color.appTextSecondary
: LinearGradient(...)
)
.cornerRadius(AppRadius.md)
.shadow(...)
}
.disabled(!isFormValid || viewModel.isLoading)
```
**New Shared Pattern:**
```swift
PrimaryButton(
title: "Log In",
icon: "arrow.right",
isLoading: viewModel.isLoading,
isDisabled: !isFormValid,
action: { viewModel.login() }
)
SecondaryButton(
title: "Cancel",
action: { dismiss() }
)
DestructiveButton(
title: "Delete",
icon: "trash",
action: { showConfirmation = true }
)
CompactButton(
title: "Edit",
icon: "pencil",
color: .appPrimary,
action: { onEdit() }
)
```
**Files to Refactor:**
- LoginView.swift (lines 222-226, 381-401)
- TaskCard.swift (lines 122-144, 151-195)
- ProfileView.swift (lines 133-145)
- ResidenceDetailView.swift
- DocumentFormView.swift
**Savings:** ~224 lines of code eliminated
---
### 9. Error Handling Pattern (16 occurrences)
**Current Pattern:**
```swift
@State private var errorAlert: ErrorAlertInfo? = nil
.onChange(of: viewModel.errorMessage) { errorMessage in
if let errorMessage = errorMessage, !errorMessage.isEmpty {
errorAlert = ErrorAlertInfo(message: errorMessage)
}
}
.errorAlert(
error: errorAlert,
onRetry: {
errorAlert = nil
submitForm()
},
onDismiss: {
errorAlert = nil
}
)
```
**New Shared Pattern:**
```swift
Form {
// content
}
.handleErrors(
error: viewModel.errorMessage,
onRetry: { viewModel.submitForm() }
)
```
**Files to Refactor:**
- TaskFormView.swift (lines 368-387)
- ProfileView.swift (lines 173-176)
- ResidenceDetailView.swift (lines 180-183)
- All ViewModels
**Savings:** ~128 lines of code eliminated
---
### 10. Form Header Pattern (14 occurrences)
**Current Pattern:**
```swift
Section {
VStack(spacing: OrganicSpacing.cozy) {
ZStack {
Circle()
.fill(
RadialGradient(
colors: [
Color.appPrimary.opacity(0.15),
Color.appPrimary.opacity(0.05),
Color.clear
],
center: .center,
startRadius: 0,
endRadius: 50
)
)
.frame(width: 100, height: 100)
Image(systemName: "person.circle.fill")
.font(.system(size: 56))
.foregroundStyle(Color.appPrimary.gradient)
}
Text("Profile Settings")
.font(.system(size: 22, weight: .bold, design: .rounded))
.foregroundColor(Color.appTextPrimary)
}
.frame(maxWidth: .infinity)
.padding(.vertical)
}
.listRowBackground(Color.clear)
```
**New Shared Pattern:**
```swift
FormHeaderSection {
OrganicFormHeader(
icon: "person.circle.fill",
title: "Profile Settings",
subtitle: "Manage your account information"
)
}
```
**Files to Refactor:**
- ProfileView.swift (lines 34-64)
- LoginView.swift (lines 52-85)
- TaskFormView.swift
- ResidenceFormView.swift
**Savings:** ~196 lines of code eliminated
---
## Total Impact
### Code Reduction
- **Total lines eliminated:** ~1,479 lines
- **Files affected:** 47 files
- **Patterns replaced:** 10 major patterns
### Benefits
1. **Maintainability:** Changes to UI patterns now require updating only ONE location
2. **Consistency:** All screens use identical styling and behavior
3. **Performance:** Centralized formatters are reused instead of recreated
4. **Type Safety:** Validation helpers provide compile-time safety
5. **Testing:** Shared utilities can be unit tested once
6. **Onboarding:** New developers reference shared utilities documentation
## Naming Conflicts Discovered
The following files already exist and would conflict with shared utilities:
1. `/Helpers/DateUtils.swift` - Keep existing, use new `Date` extension methods or `DateFormatters.shared`
2. `/Helpers/ErrorMessageParser.swift` - Keep existing enum, new struct is `SharedErrorMessageParser`
3. `/Subviews/Common/ErrorMessageView.swift` - Keep existing, use `ErrorSection` for forms
4. `/Documents/Components/EmptyStateView.swift` - Keep existing, new components have different APIs
## Migration Strategy
### Phase 1: Non-Breaking Additions (Completed)
- ✅ Create all shared utilities in `/Shared` directory
- ✅ Document usage in `/Shared/SHARED_UTILITIES.md`
- ✅ Verify build compiles successfully
### Phase 2: Gradual Adoption (Recommended)
1. Start with new features - use shared utilities for all new code
2. Refactor files during bug fixes - when touching a file, update it to use shared utilities
3. Dedicated refactor sprints - allocate time to systematically update existing files
### Phase 3: Legacy Cleanup (Future)
1. Once all files use shared utilities, remove duplicate code
2. Delete legacy helper files that are no longer needed
3. Update code review guidelines to require shared utilities
## Usage Examples
### Example 1: Refactoring a Form
**Before:**
```swift
Form {
Section {
TextField("Name", text: $name)
}
.listRowBackground(Color.appBackgroundSecondary)
if let error = errorMessage {
Section {
HStack {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(Color.appError)
Text(error)
.foregroundColor(Color.appError)
.font(.subheadline)
}
}
.listRowBackground(Color.appBackgroundSecondary)
}
Section {
Button(action: submit) {
HStack {
Spacer()
if isLoading {
ProgressView()
} else {
Text("Save")
.fontWeight(.semibold)
}
Spacer()
}
}
.disabled(isLoading)
}
.listRowBackground(Color.appBackgroundSecondary)
}
.listStyle(.plain)
.scrollContentBackground(.hidden)
.background(Color.appBackgroundPrimary)
```
**After:**
```swift
Form {
Section {
TextField("Name", text: $name)
}
.sectionBackground()
if let error = errorMessage {
ErrorSection(message: error)
}
FormActionButton(
title: "Save",
isLoading: isLoading,
action: submit
)
}
.standardFormStyle()
```
**Savings:** 26 lines → 15 lines (42% reduction)
---
### Example 2: Refactoring Date Handling
**Before:**
```swift
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
let dueDateString = dateFormatter.string(from: dueDate)
// Later...
let formatter = DateFormatter()
formatter.dateFormat = "MMM d, yyyy"
let displayString = formatter.string(from: date)
// Later...
if let date = formatter.date(from: apiString) {
let isOverdue = date < Date()
// ...
}
```
**After:**
```swift
let dueDateString = dueDate.formattedAPI()
// Later...
let displayString = date.formatted()
// Later...
let isOverdue = apiString.isOverdue()
```
**Savings:** 11 lines → 3 lines (73% reduction)
---
### Example 3: Refactoring Validation
**Before:**
```swift
func validateForm() -> Bool {
var isValid = true
if title.isEmpty {
titleError = "Title is required"
isValid = false
} else {
titleError = ""
}
if selectedCategory == nil {
viewModel.errorMessage = "Please select a category"
isValid = false
}
if selectedFrequency == nil {
viewModel.errorMessage = "Please select a frequency"
isValid = false
}
if selectedPriority == nil {
viewModel.errorMessage = "Please select a priority"
isValid = false
}
return isValid
}
```
**After:**
```swift
func validateForm() -> Bool {
let validator = FormValidator()
validator.add(fieldName: "title") {
ValidationHelpers.validateRequired(title, fieldName: "Title")
}
validator.add(fieldName: "category") {
selectedCategory != nil ? .valid : .invalid("Please select a category")
}
validator.add(fieldName: "frequency") {
selectedFrequency != nil ? .valid : .invalid("Please select a frequency")
}
validator.add(fieldName: "priority") {
selectedPriority != nil ? .valid : .invalid("Please select a priority")
}
let result = validator.validate()
if !result.isValid {
viewModel.errorMessage = result.errors.values.first
}
return result.isValid
}
```
**Benefits:** While line count is similar, validation is now centralized, testable, and provides consistent error messages.
---
## Code Review Checklist
When reviewing new code, ensure:
- [ ] Uses `.standardFormStyle()` instead of manual form styling
- [ ] Uses date extension methods instead of creating DateFormatter instances
- [ ] Uses number extensions for currency/decimal formatting
- [ ] Uses `ValidationHelpers` instead of inline validation
- [ ] Uses shared button components instead of custom styled buttons
- [ ] Uses shared empty state components instead of custom implementations
- [ ] Uses `.loadingOverlay()` instead of manual loading state UI
- [ ] Uses `.handleErrors()` instead of manual error alert handling
- [ ] Uses card modifiers instead of manual card styling
- [ ] References `/Shared/SHARED_UTILITIES.md` for examples
## Testing the Shared Utilities
All shared utilities compile successfully. To use them:
1. Import the file(s) you need (Swift automatically imports all files in the target)
2. Use the extensions and components as documented in `/Shared/SHARED_UTILITIES.md`
3. No breaking changes - existing code continues to work
Example test:
```swift
import SwiftUI
struct TestView: View {
@State private var date = Date()
var body: some View {
VStack {
Text(date.formatted()) // Uses new extension
Text(date.formattedLong())
Text(date.relativeDescription)
}
}
}
```
## Next Steps
1. **Review this document** - Understand the patterns and shared utilities
2. **Reference the usage guide** - Read `/Shared/SHARED_UTILITIES.md` for complete API documentation
3. **Start using in new code** - Use shared utilities for all new features
4. **Plan refactoring** - Schedule time to systematically update existing files
5. **Update guidelines** - Add shared utilities to code review and onboarding checklists
## Questions or Issues?
If you encounter issues or have questions about the shared utilities:
1. Check `/Shared/SHARED_UTILITIES.md` for usage examples
2. Look at the implementation in the `/Shared` files
3. The utilities are designed to be drop-in replacements for existing patterns
---
*Analysis completed: December 17, 2025*
*Files analyzed: 86 Swift files*
*Patterns identified: 10 major patterns, 266 total occurrences*

View File

@@ -1,578 +0,0 @@
# Shared Utilities and Components
This directory contains DRY (Don't Repeat Yourself) utilities, extensions, and reusable components to eliminate code duplication across the iOS app.
## Directory Structure
```
Shared/
├── Extensions/ # Swift extensions for common types
├── Components/ # Reusable UI components
├── Modifiers/ # Custom view modifiers
├── Utilities/ # Helper classes and utilities
└── README.md # This file
```
## Extensions
### ViewExtensions.swift
**Form Styling:**
- `.standardFormStyle()` - Applies consistent form styling (`.listStyle(.plain)`, `.scrollContentBackground(.hidden)`, `.background(Color.clear)`)
- `.sectionBackground()` - Applies standard section background (`.listRowBackground(Color.appBackgroundSecondary)`)
- `.headerSectionBackground()` - Applies clear background for header sections
**Loading States:**
- `.loadingOverlay(isLoading: Bool, message: String)` - Shows loading overlay with optional message
**Conditional Modifiers:**
- `.if(condition: Bool, transform:)` - Conditionally applies a modifier
- `.if(condition: Bool, if:, else:)` - Applies one of two modifiers based on condition
**Utilities:**
- `.ignoresSafeAreaAll()` - Ignores safe area for all edges
- `.dismissKeyboardOnTap()` - Dismisses keyboard when tapped
**Usage Examples:**
```swift
// Form styling
Form {
// sections
}
.standardFormStyle()
Section {
TextField("Name", text: $name)
}
.sectionBackground()
// Loading overlay
VStack {
// content
}
.loadingOverlay(isLoading: viewModel.isLoading)
// Conditional styling
Text("Hello")
.if(isHighlighted) { view in
view.foregroundColor(.red)
}
```
### DateExtensions.swift
**Date Formatting:**
- `.formatted()` - "MMM d, yyyy" (e.g., "Jan 15, 2024")
- `.formattedLong()` - "MMMM d, yyyy" (e.g., "January 15, 2024")
- `.formattedShort()` - "MM/dd/yyyy" (e.g., "01/15/2024")
- `.formattedAPI()` - "yyyy-MM-dd" (API format)
**Date Properties:**
- `.isPast` - Checks if date is in the past
- `.isToday` - Checks if date is today
- `.isTomorrow` - Checks if date is tomorrow
- `.daysFromToday` - Returns number of days from today
- `.relativeDescription` - Returns "Today", "Tomorrow", "In 3 days", etc.
**String to Date:**
- `String.toDate()` - Converts API date string to Date
- `String.toFormattedDate()` - Converts API date string to formatted display string
- `String.isOverdue()` - Checks if date string represents an overdue date
**Centralized Formatters:**
- `DateFormatters.shared.mediumDate`
- `DateFormatters.shared.longDate`
- `DateFormatters.shared.shortDate`
- `DateFormatters.shared.apiDate`
- `DateFormatters.shared.time`
- `DateFormatters.shared.dateTime`
**Usage Examples:**
```swift
// Format dates
let date = Date()
let formatted = date.formatted() // "Jan 15, 2024"
let long = date.formattedLong() // "January 15, 2024"
let api = date.formattedAPI() // "2024-01-15"
// Check date properties
if date.isPast {
print("Date is in the past")
}
let description = date.relativeDescription // "Today", "Tomorrow", etc.
// Convert string to date
let dateString = "2024-01-15"
let date = dateString.toDate()
let formatted = dateString.toFormattedDate()
```
### StringExtensions.swift
**String Utilities:**
- `.isBlank` - Checks if string is empty or whitespace
- `.nilIfBlank` - Returns nil if blank, otherwise trimmed string
- `.capitalizedFirst` - Capitalizes first letter only
- `.truncated(to: length)` - Truncates string with ellipsis
**Validation:**
- `.isValidEmail` - Validates email format
- `.isValidPhone` - Validates phone number (basic)
**Optional String:**
- `Optional<String>.isNilOrBlank` - Checks if nil or blank
- `Optional<String>.nilIfBlank` - Returns nil if blank
**Usage Examples:**
```swift
let email = " john@example.com "
if !email.isBlank {
let trimmed = email.nilIfBlank // "john@example.com"
}
if email.isValidEmail {
print("Valid email")
}
let text = "This is a very long text"
let short = text.truncated(to: 10) // "This is a..."
// Optional strings
let optional: String? = " "
if optional.isNilOrBlank {
print("String is nil or blank")
}
```
### DoubleExtensions.swift
**Number Formatting:**
- `.toCurrency()` - "$1,234.56"
- `.toCurrencyString(currencyCode:)` - Currency with custom code
- `.toDecimalString(fractionDigits:)` - "1,234.56"
- `.toPercentage(fractionDigits:)` - "45.5%"
- `.toFileSize()` - "1.2 MB"
- `.rounded(to: places)` - Rounds to decimal places
**Int Extensions:**
- `.toFormattedString()` - "1,234"
- `.toFileSize()` - "1.2 MB"
- `.pluralSuffix(singular, plural)` - Returns plural suffix
**Usage Examples:**
```swift
let price = 1234.56
let formatted = price.toCurrency() // "$1,234.56"
let percent = 45.5
let formatted = percent.toPercentage() // "45.5%"
let bytes = 1024000.0
let size = bytes.toFileSize() // "1.0 MB"
let count = 5
let text = "\(count) item\(count.pluralSuffix())" // "5 items"
```
## Components
### FormComponents.swift
**Form Header:**
- `FormHeaderSection` - Standard header with clear background
- `FormHeader` - Header with icon, title, subtitle
- `OrganicFormHeader` - Organic styled header with gradient
**Form Sections:**
- `IconFormSection` - Section with icon header
- `ErrorSection` - Displays error messages
- `SuccessSection` - Displays success messages
- `FormActionButton` - Action button section
**Form Fields:**
- `IconTextField` - Text field with icon
- `FieldLabel` - Standard field label with optional required indicator
- `FieldError` - Error message display
**Usage Examples:**
```swift
Form {
// Header
FormHeaderSection {
FormHeader(
icon: "house.fill",
title: "Add Property",
subtitle: "Enter property details"
)
}
// Field section
IconFormSection(icon: "envelope.fill", title: "Contact") {
TextField("Email", text: $email)
}
// Error display
if let error = viewModel.errorMessage {
ErrorSection(message: error)
}
// Action button
FormActionButton(
title: "Save",
isLoading: viewModel.isLoading,
action: { viewModel.save() }
)
}
.standardFormStyle()
```
### EmptyStateView.swift
**Empty State Components:**
- `EmptyStateView` - Standard empty state with icon, title, subtitle, optional action
- `OrganicEmptyState` - Organic styled empty state with blob background
- `ListEmptyState` - Compact empty state for lists
**Usage Examples:**
```swift
// Standard empty state
if items.isEmpty {
EmptyStateView(
icon: "house",
title: "No Properties",
subtitle: "Add your first property to get started",
actionLabel: "Add Property",
action: { showAddProperty = true }
)
}
// Organic empty state
OrganicEmptyState(
icon: "checkmark.circle",
title: "All Done!",
subtitle: "You have no pending tasks"
)
// List empty state
ListEmptyState(
icon: "tray",
message: "No items to display"
)
```
### ButtonStyles.swift
**Button Components:**
- `PrimaryButton` - Filled primary button
- `SecondaryButton` - Outlined secondary button
- `DestructiveButton` - Red destructive button
- `TextButton` - Text-only button
- `CompactButton` - Compact button for cards/rows
- `OrganicPrimaryButton` - Primary button with gradient and shadow
**Usage Examples:**
```swift
// Primary button
PrimaryButton(
title: "Save Changes",
icon: "checkmark",
isLoading: viewModel.isLoading,
isDisabled: !isValid,
action: { viewModel.save() }
)
// Secondary button
SecondaryButton(
title: "Cancel",
action: { dismiss() }
)
// Destructive button
DestructiveButton(
title: "Delete",
icon: "trash",
action: { showConfirmation = true }
)
// Compact button (for cards)
HStack {
CompactButton(
title: "Edit",
icon: "pencil",
color: .appPrimary,
action: { onEdit() }
)
CompactButton(
title: "Delete",
icon: "trash",
color: .appError,
isDestructive: true,
action: { onDelete() }
)
}
```
## Modifiers
### CardModifiers.swift
**Card Styling:**
- `.standardCard()` - Standard card with background, radius, padding, shadow
- `.compactCard()` - Compact card with smaller padding
- `.organicCardStyle()` - Organic card with blob background
- `.listRowCard()` - Card styling for list rows
- `.metadataPill()` - Pill styling for tags/badges
**Usage Examples:**
```swift
// Standard card
VStack {
Text("Content")
}
.standardCard()
// Compact card
HStack {
Text("Row")
}
.compactCard()
// Organic card
VStack {
Text("Feature")
}
.organicCardStyle(
accentColor: .appPrimary,
showBlob: true,
shadowIntensity: .medium
)
// Metadata pill
Text("High Priority")
.metadataPill(
backgroundColor: .appError,
foregroundColor: .white,
borderColor: .appError
)
```
## Utilities
### ValidationHelpers.swift
**Validation Methods:**
- `validateEmail(_:)` - Email validation
- `validatePassword(_:minLength:)` - Password validation
- `validatePasswordConfirmation(_:confirmation:)` - Password match
- `validateName(_:fieldName:)` - Name validation
- `validatePhone(_:)` - Phone validation
- `validateRequired(_:fieldName:)` - Required field
- `validateNumber(_:fieldName:min:max:)` - Number validation
- `validateInteger(_:fieldName:min:max:)` - Integer validation
- `validateURL(_:)` - URL validation
- `validateCustom(_:fieldName:validator:errorMessage:)` - Custom validation
**FormValidator Class:**
```swift
let validator = FormValidator()
validator.add(fieldName: "email") {
ValidationHelpers.validateEmail(email)
}
validator.add(fieldName: "password") {
ValidationHelpers.validatePassword(password, minLength: 8)
}
let result = validator.validate()
if result.isValid {
// Submit form
} else {
// Display errors
for (field, error) in result.errors {
print("\(field): \(error)")
}
}
```
**Usage Examples:**
```swift
// Single field validation
let emailResult = ValidationHelpers.validateEmail(email)
if case .invalid(let message) = emailResult {
errorMessage = message
}
// Form validation
func validateForm() -> Bool {
let validator = FormValidator()
validator.add(fieldName: "email") {
ValidationHelpers.validateEmail(email)
}
validator.add(fieldName: "password") {
ValidationHelpers.validatePassword(password)
}
let result = validator.validate()
if !result.isValid {
// Display first error
errorMessage = result.errors.values.first
return false
}
return true
}
```
### ErrorMessageParser.swift
**Error Parsing:**
- `ErrorMessageParser.parse(_: String)` - Parses error messages to user-friendly format
- `ErrorMessageParser.parse(_: Error)` - Parses Error objects
- `ErrorMessageParser.isNetworkError(_:)` - Checks if network error
- `ErrorMessageParser.isAuthError(_:)` - Checks if authentication error
**Common Error Messages:**
- `ErrorMessages.networkError`
- `ErrorMessages.unknownError`
- `ErrorMessages.timeoutError`
- `ErrorMessages.serverError`
- `ErrorMessages.unauthorizedError`
- `ErrorMessages.required(_:)` - Required field message
- `ErrorMessages.invalid(_:)` - Invalid field message
- `ErrorMessages.tooShort(_:minLength:)` - Too short message
- `ErrorMessages.tooLong(_:maxLength:)` - Too long message
**Usage Examples:**
```swift
// Parse API error
let apiError = "401 Unauthorized: Invalid token"
let userFriendly = ErrorMessageParser.parse(apiError)
// Returns: "Your session has expired. Please log in again."
// Parse Error object
do {
try await someAPICall()
} catch {
let message = ErrorMessageParser.parse(error)
showError(message)
}
// Check error type
if ErrorMessageParser.isNetworkError(errorMessage) {
// Show retry button
}
// Use common messages
errorMessage = ErrorMessages.required("Email")
// Returns: "Email is required"
```
## Migration Guide
### Before (Old Pattern)
```swift
// Old form styling
Form {
Section {
TextField("Name", text: $name)
}
.listRowBackground(Color.appBackgroundSecondary)
}
.listStyle(.plain)
.scrollContentBackground(.hidden)
.background(Color.appBackgroundPrimary)
// Old date formatting
let formatter = DateFormatter()
formatter.dateFormat = "MMM d, yyyy"
let dateString = formatter.string(from: date)
// Old validation
if email.isEmpty {
errorMessage = "Email is required"
} else if !email.contains("@") {
errorMessage = "Invalid email"
}
// Old card styling
VStack {
Text("Content")
}
.padding(16)
.background(Color.appBackgroundSecondary)
.cornerRadius(12)
.shadow(color: .black.opacity(0.1), radius: 4, y: 2)
```
### After (New Pattern)
```swift
// New form styling
Form {
Section {
TextField("Name", text: $name)
}
.sectionBackground()
}
.standardFormStyle()
// New date formatting
let dateString = date.formatted()
// New validation
let result = ValidationHelpers.validateEmail(email)
if case .invalid(let message) = result {
errorMessage = message
}
// New card styling
VStack {
Text("Content")
}
.standardCard()
```
## Best Practices
1. **Always use shared extensions** instead of creating local formatters
2. **Use validation helpers** for consistent error messages
3. **Apply view modifiers** instead of manual styling
4. **Use shared components** for common UI patterns
5. **Parse error messages** for user-friendly display
6. **Reference this README** when creating new UI components
## Testing
All shared utilities should be tested before using in production. Run the iOS app build to verify:
```bash
cd /Users/treyt/Desktop/code/HoneyDue/HoneyDueKMM
open iosApp/iosApp.xcodeproj
# Build and run (Cmd+R)
```
## Contributing
When adding new shared utilities:
1. Place in appropriate directory (Extensions, Components, Modifiers, or Utilities)
2. Add comprehensive documentation with usage examples
3. Update this README
4. Test thoroughly before committing
5. Refactor existing code to use the new utility

View File

@@ -1,97 +0,0 @@
# iOS Subviews
This folder contains all reusable SwiftUI view components, organized by feature area. Each file contains **exactly one view** with its own preview.
## 📁 Folder Structure
```
Subviews/
├── Common/ (Shared UI components)
├── Auth/ (Login/Register components)
├── Residence/ (Property-related components)
└── Task/ (Task-related components)
```
## 📋 Component List
### Common (2 components)
- **ErrorView.swift** - Full-screen error display with retry button
- **ErrorMessageView.swift** - Inline error message with dismiss button
### Auth (2 components)
- **LoginHeader.swift** - App logo and title for login screen
- **RegisterHeader.swift** - Icon and welcome text for registration
### Residence (7 components)
- **SummaryCard.swift** - Overview card with property/task statistics
- **SummaryStatView.swift** - Individual stat display (icon, value, label)
- **ResidenceCard.swift** - Property card with address and task summary
- **TaskStatChip.swift** - Small chip showing task count by status
- **EmptyResidencesView.swift** - Empty state for no properties
- **PropertyHeaderCard.swift** - Detailed property header with address
- **PropertyDetailItem.swift** - Small property detail (beds, baths, sqft)
### Task (6 components)
- **TaskPill.swift** - Small colored pill showing task counts
- **StatusBadge.swift** - Task status badge (pending, in progress, etc.)
- **PriorityBadge.swift** - Task priority badge (high, medium, low)
- **EmptyTasksView.swift** - Empty state for no tasks
- **TaskCard.swift** - Full task card with all details and actions
- **TasksSection.swift** - Complete tasks section with cancelled tasks
## 🎨 Preview Support
Every component includes a `#Preview` for easy testing in Xcode Canvas.
## ✅ Adding to Xcode Project
**IMPORTANT**: These files need to be added to the Xcode project to compile.
### Steps:
1. Open `iosApp.xcodeproj` in Xcode
2. Right-click the `iosApp` folder in Project Navigator
3. Select **"Add Files to 'iosApp'..."**
4. Navigate to and select the entire **`Subviews`** folder
5. Make sure:
- ✅ "Create groups" is selected (NOT "Create folder references")
- ✅ "Add to targets: iosApp" is checked
- ❌ "Copy items if needed" is UNchecked
6. Click **"Add"**
### Verify
Build the project (⌘+B). All 17 subview files should compile successfully.
## 🗑️ Old Files to Delete
Delete these consolidated files from the root `iosApp` directory:
-`AuthSubviews.swift`
-`CommonSubviews.swift`
-`ResidenceSubviews.swift`
-`TaskSubviews.swift`
-`DELETE_THESE_FILES.txt`
These were temporary files and have been replaced by the individual files in this `Subviews` folder.
## 📝 Usage
All main views (LoginView, ResidencesListView, ResidenceDetailView, etc.) have already been updated to use these subviews. The components are automatically available once added to the Xcode project.
Example:
```swift
import SwiftUI
struct MyView: View {
var body: some View {
VStack {
LoginHeader() // Automatically available
ErrorMessageView(message: "Error", onDismiss: {})
}
}
}
```
---
**Total Components**: 17 individual view files
**Total Previews**: 17 (one per file)
**Organization**: Feature-based folder structure