# Phase 4: Trefle API & Plant Care **Goal:** Complete care information and scheduling with local notifications **Prerequisites:** Phase 3 complete (hybrid identification working, API infrastructure established) --- ## Tasks ### 4.1 Register for Trefle API Access - [ ] Navigate to [trefle.io](https://trefle.io) - [ ] Create developer account - [ ] Generate API token - [ ] Review API documentation and rate limits - [ ] Add `TREFLE_API_TOKEN` to `APIKeys.swift`: ```swift enum APIKeys { // ... existing keys static let trefleAPIToken: String = { guard let token = Bundle.main.object(forInfoDictionaryKey: "TREFLE_API_TOKEN") as? String else { fatalError("Trefle API token not configured") } return token }() } ``` - [ ] Add `TREFLE_API_TOKEN` to Info.plist via xcconfig - [ ] Update `.xcconfig` file with Trefle token (already in .gitignore) - [ ] Verify API access with test request **Acceptance Criteria:** API token configured and accessible, test request returns valid data --- ### 4.2 Create Trefle Endpoints - [ ] Create `Data/DataSources/Remote/TrefleAPI/TrefleEndpoints.swift` - [ ] Define endpoint configuration: ```swift enum TrefleEndpoint: Endpoint { case searchPlants(query: String, page: Int) case getSpecies(slug: String) case getSpeciesById(id: Int) case getPlant(id: Int) var baseURL: URL { URL(string: "https://trefle.io/api/v1")! } var path: String { switch self { case .searchPlants: return "/plants/search" case .getSpecies(let slug): return "/species/\(slug)" case .getSpeciesById(let id): return "/species/\(id)" case .getPlant(let id): return "/plants/\(id)" } } var method: HTTPMethod { .get } var queryItems: [URLQueryItem] { var items = [URLQueryItem(name: "token", value: APIKeys.trefleAPIToken)] switch self { case .searchPlants(let query, let page): items.append(URLQueryItem(name: "q", value: query)) items.append(URLQueryItem(name: "page", value: String(page))) default: break } return items } } ``` - [ ] Support pagination for search results - [ ] Add filter parameters (edible, vegetable, etc.) **Acceptance Criteria:** Endpoints build correct URLs with token and query parameters --- ### 4.3 Implement Trefle API Service - [ ] Create `Data/DataSources/Remote/TrefleAPI/TrefleAPIService.swift` - [ ] Define protocol: ```swift protocol TrefleAPIServiceProtocol: Sendable { func searchPlants(query: String, page: Int) async throws -> TrefleSearchResponseDTO func getSpecies(slug: String) async throws -> TrefleSpeciesResponseDTO func getSpeciesById(id: Int) async throws -> TrefleSpeciesResponseDTO } ``` - [ ] Implement service using NetworkService: - Handle token-based authentication - Parse paginated responses - Handle 404 for unknown species - [ ] Implement retry logic (1 retry with exponential backoff) - [ ] Add request timeout (15 seconds) - [ ] Handle rate limiting (120 requests/minute) - [ ] Log request/response for debugging **Acceptance Criteria:** Service retrieves species data and handles errors gracefully --- ### 4.4 Create Trefle DTOs - [ ] Create `Data/DataSources/Remote/TrefleAPI/DTOs/TrefleDTOs.swift` - [ ] Define response DTOs: ```swift struct TrefleSearchResponseDTO: Decodable { let data: [TreflePlantSummaryDTO] let links: TrefleLinksDTO let meta: TrefleMetaDTO } struct TrefleSpeciesResponseDTO: Decodable { let data: TrefleSpeciesDTO let meta: TrefleMetaDTO } ``` - [ ] Create `TrefleSpeciesDTO`: ```swift struct TrefleSpeciesDTO: Decodable { let id: Int let commonName: String? let slug: String let scientificName: String let year: Int? let bibliography: String? let author: String? let familyCommonName: String? let family: String? let genus: String? let genusId: Int? let imageUrl: String? let images: TrefleImagesDTO? let distribution: TrefleDistributionDTO? let specifications: TrefleSpecificationsDTO? let growth: TrefleGrowthDTO? let synonyms: [TrefleSynonymDTO]? let sources: [TrefleSourceDTO]? } ``` - [ ] Create `TrefleGrowthDTO`: ```swift struct TrefleGrowthDTO: Decodable { let description: String? let sowing: String? let daysToHarvest: Int? let rowSpacing: TrefleMeasurementDTO? let spread: TrefleMeasurementDTO? let phMaximum: Double? let phMinimum: Double? let light: Int? // 0-10 scale let atmosphericHumidity: Int? // 0-10 scale let growthMonths: [String]? let bloomMonths: [String]? let fruitMonths: [String]? let minimumPrecipitation: TrefleMeasurementDTO? let maximumPrecipitation: TrefleMeasurementDTO? let minimumRootDepth: TrefleMeasurementDTO? let minimumTemperature: TrefleMeasurementDTO? let maximumTemperature: TrefleMeasurementDTO? let soilNutriments: Int? // 0-10 scale let soilSalinity: Int? // 0-10 scale let soilTexture: Int? // 0-10 scale let soilHumidity: Int? // 0-10 scale } ``` - [ ] Create supporting DTOs: `TrefleSpecificationsDTO`, `TrefleImagesDTO`, `TrefleMeasurementDTO` - [ ] Add CodingKeys for snake_case API responses - [ ] Write unit tests for DTO decoding **Acceptance Criteria:** DTOs decode actual Trefle API responses without errors --- ### 4.5 Build Trefle Mapper - [ ] Create `Data/Mappers/TrefleMapper.swift` - [ ] Implement mapping functions: ```swift struct TrefleMapper { static func mapToPlantCareSchedule( from species: TrefleSpeciesDTO, plantID: UUID ) -> PlantCareSchedule static func mapToLightRequirement( from light: Int? ) -> LightRequirement static func mapToWateringSchedule( from growth: TrefleGrowthDTO? ) -> WateringSchedule static func mapToTemperatureRange( from growth: TrefleGrowthDTO? ) -> TemperatureRange static func mapToFertilizerSchedule( from growth: TrefleGrowthDTO? ) -> FertilizerSchedule? static func generateCareTasks( from schedule: PlantCareSchedule, startDate: Date ) -> [CareTask] } ``` - [ ] Map Trefle light scale (0-10) to `LightRequirement`: ```swift enum LightRequirement: String, Codable, Sendable { case fullShade // 0-2 case partialShade // 3-4 case partialSun // 5-6 case fullSun // 7-10 var description: String { ... } var hoursOfLight: ClosedRange { ... } } ``` - [ ] Map humidity/precipitation to `WateringSchedule`: ```swift struct WateringSchedule: Codable, Sendable { let frequency: WateringFrequency let amount: WateringAmount let seasonalAdjustments: [Season: WateringFrequency]? enum WateringFrequency: String, Codable, Sendable { case daily, everyOtherDay, twiceWeekly, weekly, biweekly, monthly var intervalDays: Int { ... } } enum WateringAmount: String, Codable, Sendable { case light, moderate, thorough, soak } } ``` - [ ] Map temperature data to `TemperatureRange`: ```swift struct TemperatureRange: Codable, Sendable { let minimum: Measurement let maximum: Measurement let optimal: Measurement? let frostTolerant: Bool } ``` - [ ] Map soil nutrients to `FertilizerSchedule`: ```swift struct FertilizerSchedule: Codable, Sendable { let frequency: FertilizerFrequency let type: FertilizerType let seasonalApplication: Bool let activeMonths: [Int]? // 1-12 enum FertilizerFrequency: String, Codable, Sendable { case weekly, biweekly, monthly, quarterly, biannually } enum FertilizerType: String, Codable, Sendable { case balanced, highNitrogen, highPhosphorus, highPotassium, organic } } ``` - [ ] Handle missing data with sensible defaults - [ ] Unit test all mapping functions **Acceptance Criteria:** Mapper produces valid care schedules from all Trefle response variations --- ### 4.6 Implement Fetch Plant Care Use Case - [ ] Create `Domain/UseCases/PlantCare/FetchPlantCareUseCase.swift` - [ ] Define protocol: ```swift protocol FetchPlantCareUseCaseProtocol: Sendable { func execute(scientificName: String) async throws -> PlantCareInfo func execute(trefleId: Int) async throws -> PlantCareInfo } ``` - [ ] Define `PlantCareInfo` domain entity: ```swift struct PlantCareInfo: Identifiable, Sendable { let id: UUID let scientificName: String let commonName: String? let lightRequirement: LightRequirement let wateringSchedule: WateringSchedule let temperatureRange: TemperatureRange let fertilizerSchedule: FertilizerSchedule? let soilType: SoilType? let humidity: HumidityLevel? let growthRate: GrowthRate? let bloomingSeason: [Season]? let additionalNotes: String? let sourceURL: URL? } ``` - [ ] Implement use case: - Search Trefle by scientific name - Fetch detailed species data - Map to domain entity - Cache results for offline access - [ ] Handle species not found in Trefle - [ ] Add fallback to generic care data for unknown species - [ ] Register in DIContainer **Acceptance Criteria:** Use case retrieves care data, handles missing species gracefully --- ### 4.7 Create Care Schedule Use Case - [ ] Create `Domain/UseCases/PlantCare/CreateCareScheduleUseCase.swift` - [ ] Define protocol: ```swift protocol CreateCareScheduleUseCaseProtocol: Sendable { func execute( for plant: Plant, careInfo: PlantCareInfo, userPreferences: CarePreferences? ) async throws -> PlantCareSchedule } ``` - [ ] Define `CarePreferences`: ```swift struct CarePreferences: Codable, Sendable { let preferredWateringTime: DateComponents // e.g., 8:00 AM let reminderDaysBefore: Int // remind N days before task let groupWateringDays: Bool // water all plants same day let adjustForSeason: Bool let location: PlantLocation? enum PlantLocation: String, Codable, Sendable { case indoor, outdoor, greenhouse, balcony } } ``` - [ ] Implement schedule generation: - Calculate next N watering dates (30 days ahead) - Calculate fertilizer dates based on schedule - Adjust for seasons if enabled - Create `CareTask` entities for each scheduled item - [ ] Define `CareTask` entity: ```swift struct CareTask: Identifiable, Codable, Sendable { let id: UUID let plantID: UUID let type: CareTaskType let scheduledDate: Date let isCompleted: Bool let completedDate: Date? let notes: String? enum CareTaskType: String, Codable, Sendable { case watering, fertilizing, pruning, repotting, pestControl, rotation var icon: String { ... } var defaultReminderOffset: TimeInterval { ... } } } ``` - [ ] Persist schedule to Core Data - [ ] Register in DIContainer **Acceptance Criteria:** Use case creates complete care schedule with future tasks --- ### 4.8 Build Plant Detail View - [ ] Create `Presentation/Scenes/PlantDetail/PlantDetailView.swift` - [ ] Create `PlantDetailViewModel`: ```swift @Observable final class PlantDetailViewModel { private(set) var plant: Plant private(set) var careInfo: PlantCareInfo? private(set) var careSchedule: PlantCareSchedule? private(set) var isLoading: Bool = false private(set) var error: Error? func loadCareInfo() async func createSchedule(preferences: CarePreferences?) async func markTaskComplete(_ task: CareTask) async } ``` - [ ] Implement view sections: ```swift struct PlantDetailView: View { @State private var viewModel: PlantDetailViewModel var body: some View { ScrollView { PlantHeaderSection(plant: viewModel.plant) IdentificationSection(plant: viewModel.plant) CareInformationSection(careInfo: viewModel.careInfo) UpcomingTasksSection(tasks: viewModel.upcomingTasks) CareScheduleSection(schedule: viewModel.careSchedule) } } } ``` - [ ] Create `CareInformationSection` component: ```swift struct CareInformationSection: View { let careInfo: PlantCareInfo? var body: some View { Section("Care Requirements") { LightRequirementRow(requirement: careInfo?.lightRequirement) WateringRow(schedule: careInfo?.wateringSchedule) TemperatureRow(range: careInfo?.temperatureRange) FertilizerRow(schedule: careInfo?.fertilizerSchedule) HumidityRow(level: careInfo?.humidity) } } } ``` - [ ] Create care info row components: - `LightRequirementRow` - sun icon, description, hours - `WateringRow` - drop icon, frequency, amount - `TemperatureRow` - thermometer, min/max/optimal - `FertilizerRow` - leaf icon, frequency, type - `HumidityRow` - humidity icon, level indicator - [ ] Add loading skeleton for care info - [ ] Handle "care data unavailable" state - [ ] Implement pull-to-refresh **Acceptance Criteria:** Detail view displays all plant info with care requirements --- ### 4.9 Implement Care Schedule View - [ ] Create `Presentation/Scenes/CareSchedule/CareScheduleView.swift` - [ ] Create `CareScheduleViewModel`: ```swift @Observable final class CareScheduleViewModel { private(set) var upcomingTasks: [CareTask] = [] private(set) var tasksByDate: [Date: [CareTask]] = [:] private(set) var plants: [Plant] = [] var selectedFilter: TaskFilter = .all enum TaskFilter: CaseIterable { case all, watering, fertilizing, overdue, today } func loadTasks() async func markComplete(_ task: CareTask) async func snoozeTask(_ task: CareTask, until: Date) async func skipTask(_ task: CareTask) async } ``` - [ ] Implement main schedule view: ```swift struct CareScheduleView: View { @State private var viewModel: CareScheduleViewModel var body: some View { NavigationStack { List { OverdueTasksSection(tasks: viewModel.overdueTasks) TodayTasksSection(tasks: viewModel.todayTasks) UpcomingTasksSection(tasksByDate: viewModel.upcomingByDate) } .navigationTitle("Care Schedule") .toolbar { FilterMenu(selection: $viewModel.selectedFilter) } } } } ``` - [ ] Create `CareTaskRow` component: ```swift struct CareTaskRow: View { let task: CareTask let plant: Plant let onComplete: () -> Void let onSnooze: (Date) -> Void var body: some View { HStack { PlantThumbnail(plant: plant) VStack(alignment: .leading) { Text(plant.commonNames.first ?? plant.scientificName) Text(task.type.rawValue.capitalized) .font(.caption) .foregroundStyle(.secondary) } Spacer() TaskActionButtons(...) } .swipeActions { ... } } } ``` - [ ] Implement calendar view option: ```swift struct CareCalendarView: View { let tasksByDate: [Date: [CareTask]] @Binding var selectedDate: Date var body: some View { VStack { CalendarGrid(tasksByDate: tasksByDate, selection: $selectedDate) TaskListForDate(tasks: tasksByDate[selectedDate] ?? []) } } } ``` - [ ] Add empty state for "no tasks scheduled" - [ ] Implement batch actions (complete all today's watering) - [ ] Add quick-add task functionality **Acceptance Criteria:** Schedule view shows all upcoming tasks, supports filtering and completion --- ### 4.10 Add Local Notifications for Care Reminders - [ ] Create `Core/Services/NotificationService.swift` - [ ] Define protocol: ```swift protocol NotificationServiceProtocol: Sendable { func requestAuthorization() async throws -> Bool func scheduleReminder(for task: CareTask, plant: Plant) async throws func cancelReminder(for task: CareTask) async func cancelAllReminders(for plantID: UUID) async func updateBadgeCount() async func getPendingNotifications() async -> [UNNotificationRequest] } ``` - [ ] Implement notification service: ```swift final class NotificationService: NotificationServiceProtocol { private let center = UNUserNotificationCenter.current() func scheduleReminder(for task: CareTask, plant: Plant) async throws { let content = UNMutableNotificationContent() content.title = "Plant Care Reminder" content.body = "\(plant.commonNames.first ?? plant.scientificName) needs \(task.type.rawValue)" content.sound = .default content.badge = await calculateBadgeCount() as NSNumber content.userInfo = [ "taskID": task.id.uuidString, "plantID": plant.id.uuidString, "taskType": task.type.rawValue ] content.categoryIdentifier = "CARE_REMINDER" let trigger = UNCalendarNotificationTrigger( dateMatching: Calendar.current.dateComponents( [.year, .month, .day, .hour, .minute], from: task.scheduledDate ), repeats: false ) let request = UNNotificationRequest( identifier: "care-\(task.id.uuidString)", content: content, trigger: trigger ) try await center.add(request) } } ``` - [ ] Set up notification categories and actions: ```swift func setupNotificationCategories() { let completeAction = UNNotificationAction( identifier: "COMPLETE", title: "Mark Complete", options: .foreground ) let snoozeAction = UNNotificationAction( identifier: "SNOOZE", title: "Snooze 1 Hour", options: [] ) let category = UNNotificationCategory( identifier: "CARE_REMINDER", actions: [completeAction, snoozeAction], intentIdentifiers: [], options: .customDismissAction ) UNUserNotificationCenter.current().setNotificationCategories([category]) } ``` - [ ] Handle notification responses in app delegate/scene delegate - [ ] Create `ScheduleNotificationsUseCase`: ```swift protocol ScheduleNotificationsUseCaseProtocol: Sendable { func scheduleAll(for schedule: PlantCareSchedule, plant: Plant) async throws func rescheduleAll() async throws // Call after task completion func syncWithSystem() async // Verify scheduled vs expected } ``` - [ ] Add notification settings UI: - Enable/disable reminders - Set default reminder time - Set advance notice period - Sound selection - [ ] Handle notification permission denied gracefully - [ ] Register in DIContainer **Acceptance Criteria:** Notifications fire at scheduled times with actionable buttons --- ## End-of-Phase Validation ### Functional Verification | Test | Steps | Expected Result | Status | |------|-------|-----------------|--------| | API Token Configured | Build app | No crash on Trefle token access | [ ] | | Plant Search | Search "Monstera" | Returns matching species | [ ] | | Species Detail | Fetch species by slug | Returns complete growth data | [ ] | | Care Info Display | View identified plant | Care requirements shown | [ ] | | Schedule Creation | Add plant to collection | Care schedule generated | [ ] | | Task List | Open care schedule tab | Upcoming tasks displayed | [ ] | | Task Completion | Tap complete on task | Task marked done, removed from list | [ ] | | Task Snooze | Snooze task 1 hour | Task rescheduled, notification updated | [ ] | | Notification Permission | First launch | Permission dialog shown | [ ] | | Notification Delivery | Wait for scheduled time | Notification appears | [ ] | | Notification Action | Tap "Mark Complete" | App opens, task completed | [ ] | | Offline Care Data | Disable network | Cached care info displayed | [ ] | | Unknown Species | Search non-existent plant | Graceful "not found" message | [ ] | | Calendar View | Switch to calendar | Tasks shown on correct dates | [ ] | | Filter Tasks | Filter by "watering" | Only watering tasks shown | [ ] | ### Code Quality Verification | Check | Criteria | Status | |-------|----------|--------| | Build | Project builds with zero warnings | [ ] | | Architecture | Trefle code isolated in Data/DataSources/Remote/TrefleAPI/ | [ ] | | Protocols | All services use protocols for testability | [ ] | | Sendable | All new types conform to Sendable | [ ] | | DTOs | DTOs decode sample Trefle responses correctly | [ ] | | Mapper | Mapper handles all optional fields with defaults | [ ] | | Use Cases | Business logic in use cases, not ViewModels | [ ] | | DI Container | New services registered in container | [ ] | | Error Types | Trefle-specific errors defined | [ ] | | Unit Tests | DTOs, mappers, and use cases have tests | [ ] | | Secrets | API token not in source control | [ ] | | Notifications | Permission handling follows Apple guidelines | [ ] | ### Performance Verification | Metric | Target | Actual | Status | |--------|--------|--------|--------| | Trefle Search Response | < 2 seconds | | [ ] | | Species Detail Fetch | < 3 seconds | | [ ] | | Care Schedule Generation | < 100ms | | [ ] | | Plant Detail View Load | < 500ms | | [ ] | | Care Schedule View Load | < 300ms | | [ ] | | Notification Scheduling (batch) | < 1 second for 10 tasks | | [ ] | | Care Info Cache Lookup | < 50ms | | [ ] | | Calendar View Render | < 200ms | | [ ] | ### API Integration Verification | Test | Steps | Expected Result | Status | |------|-------|-----------------|--------| | Valid Species | Search "Quercus robur" | Returns oak species data | [ ] | | Growth Data Present | Fetch species with growth | Light, water, temp data present | [ ] | | Growth Data Missing | Fetch species without growth | Defaults used, no crash | [ ] | | Pagination | Search common term | Multiple pages available | [ ] | | Rate Limiting | Make rapid requests | 429 handled gracefully | [ ] | | Invalid Token | Use wrong token | Unauthorized error shown | [ ] | | Species Not Found | Search gibberish | Empty results, no error | [ ] | | Image URLs | Fetch species | Valid image URLs returned | [ ] | ### Care Schedule Verification | Scenario | Input | Expected Output | Status | |----------|-------|-----------------|--------| | Daily Watering | High humidity plant | Tasks every day | [ ] | | Weekly Watering | Low humidity plant | Tasks every 7 days | [ ] | | Monthly Fertilizer | High nutrient need | Tasks every 30 days | [ ] | | No Fertilizer | Low nutrient need | No fertilizer tasks | [ ] | | Seasonal Adjustment | Outdoor plant in winter | Reduced watering frequency | [ ] | | User Preferred Time | Set 9:00 AM | All tasks at 9:00 AM | [ ] | | 30-Day Lookahead | Create schedule | Tasks for next 30 days | [ ] | | Task Completion | Complete watering | Next occurrence scheduled | [ ] | | Plant Deletion | Delete plant | All tasks removed | [ ] | ### Notification Verification | Test | Steps | Expected Result | Status | |------|-------|-----------------|--------| | Permission Granted | Accept notification prompt | Reminders scheduled | [ ] | | Permission Denied | Deny notification prompt | Graceful fallback, in-app alerts | [ ] | | Notification Content | Receive notification | Correct plant name and task type | [ ] | | Complete Action | Tap "Mark Complete" | Task completed in app | [ ] | | Snooze Action | Tap "Snooze" | Notification rescheduled | [ ] | | Badge Count | Have 3 overdue tasks | Badge shows 3 | [ ] | | Badge Clear | Complete all tasks | Badge cleared | [ ] | | Background Delivery | App closed | Notification still fires | [ ] | | Notification Tap | Tap notification | Opens plant detail | [ ] | | Bulk Reschedule | Complete task | Future notifications updated | [ ] | --- ## Phase 4 Completion Checklist - [ ] All 10 tasks completed (core implementation) - [ ] All functional tests pass - [ ] All code quality checks pass - [ ] All performance targets met - [ ] Trefle API integration verified - [ ] Care schedule generation working - [ ] Task management (complete/snooze/skip) working - [ ] Notifications scheduling and firing correctly - [ ] Notification actions handled properly - [ ] Offline mode works (cached care data) - [ ] API token secured (not in git) - [ ] Unit tests for DTOs, mappers, and use cases - [ ] UI tests for critical flows (view plant, complete task) - [ ] Code committed with descriptive message - [ ] Ready for Phase 5 (Plant Collection & Persistence) --- ## Error Handling ### Trefle API Errors ```swift enum TrefleAPIError: Error, LocalizedError { case invalidToken case rateLimitExceeded case speciesNotFound(query: String) case serverError(statusCode: Int) case networkUnavailable case timeout case invalidResponse case paginationExhausted var errorDescription: String? { switch self { case .invalidToken: return "Invalid API token. Please check configuration." case .rateLimitExceeded: return "Too many requests. Please wait a moment." case .speciesNotFound(let query): return "No species found matching '\(query)'." case .serverError(let code): return "Server error (\(code)). Please try again later." case .networkUnavailable: return "No network connection." case .timeout: return "Request timed out. Please try again." case .invalidResponse: return "Invalid response from server." case .paginationExhausted: return "No more results available." } } } ``` ### Care Schedule Errors ```swift enum CareScheduleError: Error, LocalizedError { case noCareDataAvailable case schedulePersistenceFailed case invalidDateRange case plantNotFound var errorDescription: String? { switch self { case .noCareDataAvailable: return "Care information not available for this plant." case .schedulePersistenceFailed: return "Failed to save care schedule." case .invalidDateRange: return "Invalid date range for schedule." case .plantNotFound: return "Plant not found in collection." } } } ``` ### Notification Errors ```swift enum NotificationError: Error, LocalizedError { case permissionDenied case schedulingFailed case invalidTriggerDate case categoryNotRegistered var errorDescription: String? { switch self { case .permissionDenied: return "Notification permission denied. Enable in Settings." case .schedulingFailed: return "Failed to schedule reminder." case .invalidTriggerDate: return "Cannot schedule reminder for past date." case .categoryNotRegistered: return "Notification category not configured." } } } ``` --- ## Notes - Trefle API has growth data for ~10% of species; implement graceful fallbacks - Cache Trefle responses aggressively (data rarely changes) - Notification limit: iOS allows ~64 pending local notifications - Schedule notifications in batches to stay under limit - Use background app refresh to reschedule notifications periodically - Consider user's timezone for notification scheduling - Trefle measurement units vary; normalize to metric internally, display in user's preference - Some plants need seasonal care adjustments (reduce watering in winter) - Badge count should only reflect overdue tasks, not all pending - Test notification actions with app in foreground, background, and terminated states --- ## Dependencies | Dependency | Type | Notes | |------------|------|-------| | Trefle API | External API | 120 req/min rate limit | | UserNotifications | System | Local notifications | | URLSession | System | API requests | | Core Data | System | Schedule persistence | --- ## Risk Mitigation | Risk | Mitigation | |------|------------| | Trefle API token exposed | Use xcconfig, add to .gitignore | | Species not in Trefle | Provide generic care defaults | | Missing growth data | Use conservative defaults for watering/light | | Notification permission denied | In-app task list always available | | Too many notifications | Limit to 64, prioritize soonest tasks | | User ignores reminders | Badge count, overdue section in UI | | Trefle API downtime | Cache responses, retry with backoff | | Incorrect care recommendations | Add disclaimer, allow user overrides | | Timezone issues | Store all dates in UTC, convert for display | | App deleted with pending notifications | Notifications orphaned (OS handles cleanup) | --- ## Sample Trefle API Response ### Search Response ```json { "data": [ { "id": 834, "common_name": "Swiss cheese plant", "slug": "monstera-deliciosa", "scientific_name": "Monstera deliciosa", "year": 1849, "bibliography": "Vidensk. Meddel. Naturhist. Foren. Kjøbenhavn 1849: 19 (1849)", "author": "Liebm.", "family_common_name": "Arum family", "genus_id": 1254, "image_url": "https://bs.plantnet.org/image/o/abc123", "genus": "Monstera", "family": "Araceae" } ], "links": { "self": "/api/v1/plants/search?q=monstera", "first": "/api/v1/plants/search?page=1&q=monstera", "last": "/api/v1/plants/search?page=1&q=monstera" }, "meta": { "total": 12 } } ``` ### Species Detail Response ```json { "data": { "id": 834, "common_name": "Swiss cheese plant", "slug": "monstera-deliciosa", "scientific_name": "Monstera deliciosa", "growth": { "light": 6, "atmospheric_humidity": 8, "minimum_temperature": { "deg_c": 15 }, "maximum_temperature": { "deg_c": 30 }, "soil_humidity": 7, "soil_nutriments": 5 }, "specifications": { "growth_rate": "moderate", "toxicity": "mild" } }, "meta": { "last_modified": "2023-01-15T12:00:00Z" } } ``` --- ## UI Mockups (Conceptual) ### Plant Detail - Care Section ``` ┌─────────────────────────────────────┐ │ ☀️ Light: Partial Sun (5-6 hrs) │ │ 💧 Water: Twice Weekly (Moderate) │ │ 🌡️ Temp: 15-30°C (Optimal: 22°C) │ │ 🌱 Fertilizer: Monthly (Balanced) │ │ 💨 Humidity: High │ └─────────────────────────────────────┘ ``` ### Care Schedule - Task List ``` ┌─────────────────────────────────────┐ │ OVERDUE (2) │ │ ┌─────────────────────────────────┐ │ │ │ 🪴 Monstera 💧 Water [✓] │ │ │ │ 🪴 Pothos 💧 Water [✓] │ │ │ └─────────────────────────────────┘ │ │ │ │ TODAY │ │ ┌─────────────────────────────────┐ │ │ │ 🪴 Ficus 🌱 Fertilize [✓]│ │ │ └─────────────────────────────────┘ │ │ │ │ TOMORROW │ │ ┌─────────────────────────────────┐ │ │ │ 🪴 Snake Plant 💧 Water [○] │ │ │ └─────────────────────────────────┘ │ └─────────────────────────────────────┘ ```