- Implement camera capture and plant identification workflow - Add Core Data persistence for plants, care schedules, and cached API data - Create collection view with grid/list layouts and filtering - Build plant detail views with care information display - Integrate Trefle botanical API for plant care data - Add local image storage for captured plant photos - Implement dependency injection container for testability - Include accessibility support throughout the app Bug fixes in this commit: - Fix Trefle API decoding by removing duplicate CodingKeys - Fix LocalCachedImage to load from correct PlantImages directory - Set dateAdded when saving plants for proper collection sorting Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
32 KiB
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
- Create developer account
- Generate API token
- Review API documentation and rate limits
- Add
TREFLE_API_TOKENtoAPIKeys.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_TOKENto Info.plist via xcconfig - Update
.xcconfigfile 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:
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:
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:
struct TrefleSearchResponseDTO: Decodable { let data: [TreflePlantSummaryDTO] let links: TrefleLinksDTO let meta: TrefleMetaDTO } struct TrefleSpeciesResponseDTO: Decodable { let data: TrefleSpeciesDTO let meta: TrefleMetaDTO } - Create
TrefleSpeciesDTO: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: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:
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: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<Int> { ... } } - Map humidity/precipitation to
WateringSchedule: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:struct TemperatureRange: Codable, Sendable { let minimum: Measurement<UnitTemperature> let maximum: Measurement<UnitTemperature> let optimal: Measurement<UnitTemperature>? let frostTolerant: Bool } - Map soil nutrients to
FertilizerSchedule: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:
protocol FetchPlantCareUseCaseProtocol: Sendable { func execute(scientificName: String) async throws -> PlantCareInfo func execute(trefleId: Int) async throws -> PlantCareInfo } - Define
PlantCareInfodomain entity: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:
protocol CreateCareScheduleUseCaseProtocol: Sendable { func execute( for plant: Plant, careInfo: PlantCareInfo, userPreferences: CarePreferences? ) async throws -> PlantCareSchedule } - Define
CarePreferences: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
CareTaskentities for each scheduled item
- Define
CareTaskentity: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:@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:
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
CareInformationSectioncomponent: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, hoursWateringRow- drop icon, frequency, amountTemperatureRow- thermometer, min/max/optimalFertilizerRow- leaf icon, frequency, typeHumidityRow- 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:@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:
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
CareTaskRowcomponent: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:
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:
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:
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:
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: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
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
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
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
{
"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
{
"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 [○] │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────┘