diff --git a/CLAUDE.md b/CLAUDE.md index 93cac6e..e7adf55 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,48 +1,431 @@ -# Feels - Claude Code Context +# Feels -## Project Summary +iOS mood tracking app. Users rate their day on a 5-point scale (Horrible to Great) and view patterns via Day, Month, Year, and Insights views. Includes watchOS companion, widgets, Live Activities, and AI-powered insights. -Feels is an iOS mood tracking app. Users rate their day on a 5-point scale (Horrible → Great) and view patterns via Day, Month, and Year views. - -## Architecture - -- **Pattern**: MVVM with SwiftUI -- **Data**: Core Data with CloudKit sync -- **Monetization**: StoreKit 2 subscriptions (30-day trial, monthly/yearly plans) - -## Key Directories - -``` -Shared/ # Core app code (Models, Views, Persistence) -FeelsWidget2/ # Widget extension -Feels Watch App/ # watchOS companion -docs/ # ASO and competitive analysis -``` - -## Data Layer - -Core Data operations are split across files in `Shared/Persistence/`: -- `Persistence.swift` - Core Data stack setup -- `PersistenceGET.swift` - Fetch operations -- `PersistenceADD.swift` - Create entries -- `PersistenceUPDATE.swift` - Update operations -- `PersistenceDELETE.swift` - Delete operations - -## App Groups - -- **Production**: `group.com.88oakapps.feels` -- **Debug**: `group.com.88oakapps.feels.debug` - -## Build & Run +## Build & Run Commands ```bash -# Build the app +# Build xcodebuild -project Feels.xcodeproj -scheme "Feels (iOS)" -destination 'platform=iOS Simulator,name=iPhone 16 Pro' build -# Run tests +# Run all tests xcodebuild -project Feels.xcodeproj -scheme "Feels (iOS)" -destination 'platform=iOS Simulator,name=iPhone 16 Pro' test + +# Run a single test suite +xcodebuild -project Feels.xcodeproj -scheme "Feels (iOS)" -destination 'platform=iOS Simulator,name=iPhone 16 Pro' -only-testing:"Tests iOS/Tests_iOS" test + +# Run a single test +xcodebuild -project Feels.xcodeproj -scheme "Feels (iOS)" -destination 'platform=iOS Simulator,name=iPhone 16 Pro' -only-testing:"Tests iOS/Tests_iOS/testDatesBetween" test ``` +## Architecture Overview + +- **Pattern**: MVVM with SwiftUI +- **Language/Framework**: Swift / SwiftUI +- **Data**: SwiftData with CloudKit sync (migrated from Core Data) +- **Test Framework**: XCTest (minimal coverage currently) + +### Layers + +1. **Models** (`Shared/Models/`): Data types and domain logic + - `MoodEntryModel` — SwiftData `@Model` for mood entries + - `Mood` — Enum for mood values (horrible/bad/average/good/great/missing/placeholder) + - `EntryType` — Enum for entry source (listView/widget/watch/shortcut/siri/controlCenter/liveActivity/etc.) + - Customization protocols: `MoodTintable`, `MoodImagable`, `PersonalityPackable`, `Themeable` + +2. **Persistence** (`Shared/Persisence/`): SwiftData operations (note: directory has a typo — "Persisence" not "Persistence") + - `DataController` — Singleton `@MainActor` data access layer + - `DataControllerGET` — Read/fetch operations + - `DataControllerADD` — Create operations + - `DataControllerUPDATE` — Update operations + - `DataControllerDELETE` — Delete operations + - `DataControllerHelper` — Utility methods + - `DataControllerProtocol` — Protocol definitions for testability + - `SharedModelContainer` — Factory for `ModelContainer` (shared App Group storage) + - `ExtensionDataProvider` — Data provider for widget and watch extensions + +3. **ViewModels** (colocated with views): + - `DayViewViewModel` — Day/Month grid data, mood logging + - `YearViewModel` — Year view filtering and chart data + - `InsightsViewModel` — AI-powered insights via Apple Foundation Models + - `IconViewModel` — Custom app icon selection + - `CustomWidgetStateViewModel` — Widget customization state + +4. **Views** (`Shared/Views/`): SwiftUI views organized by feature + - `DayView/`, `MonthView/`, `YearView/`, `InsightsView/` + - `SettingsView/`, `CustomizeView/`, `CustomWidget/` + - `Sharing/`, `SharingTemplates/` + +5. **Services** (`Shared/Services/` and `Shared/`): Business logic singletons + - `MoodLogger` — Centralized mood logging with side effects + - `IAPManager` — StoreKit 2 subscriptions + - `AnalyticsManager` — PostHog analytics + - `HealthKitManager` — HealthKit mood sync + - `BiometricAuthManager` — Face ID / Touch ID + - `PhotoManager` — Photo attachment handling + - `WatchConnectivityManager` — Watch-phone communication + - `LiveActivityScheduler` — Live Activity lifecycle + - `ReviewRequestManager` — App Store review prompts + - `FoundationModelsInsightService` — Apple Foundation Models AI + +### Key Components + +| Component | Type | Responsibility | +|-----------|------|----------------| +| `DataController.shared` | @MainActor singleton | All SwiftData CRUD — single source of truth for data access | +| `MoodLogger.shared` | @MainActor singleton | Centralized mood logging with all side effects (HealthKit, streak, widget, watch, Live Activity) | +| `IAPManager.shared` | @MainActor ObservableObject | StoreKit 2 subscription state, trial tracking, paywall gating | +| `AnalyticsManager.shared` | @MainActor singleton | PostHog event tracking — all analytics go through this | +| `ExtensionDataProvider.shared` | @MainActor singleton | Widget/Watch data access via App Group container | +| `HealthKitManager.shared` | ObservableObject | HealthKit read/write for mood data | +| `WatchConnectivityManager.shared` | ObservableObject (NSObject) | WCSession for phone-watch UI updates | +| `LiveActivityScheduler.shared` | ObservableObject | Manages mood streak Live Activity start/stop timing | + +### Data Flow + +``` +User taps mood → DayViewViewModel.add() + ↓ + MoodLogger.shared.logMood() ← ALL mood entry points use this + ↓ + DataController.shared.add() ← SwiftData insert + save + ↓ + Side effects (all in MoodLogger): + → HealthKit sync (if enabled + subscribed) + → Streak calculation + → Live Activity update + → Widget reload (WidgetCenter.shared.reloadAllTimelines()) + → Watch notification (WatchConnectivityManager) + → TipKit parameter update + → Analytics event + +Widget/Watch → ExtensionDataProvider.shared ← Separate container via App Group + ↓ + MoodLogger.applySideEffects() ← Catch-up when app opens + +CloudKit ←→ DataController.container ← Automatic sync via SwiftData CloudKit integration +``` + +## Data Access Rules + +**Source of Truth**: `DataController.shared` — all data reads and writes go through this singleton. + +### Correct Usage + +```swift +// ✅ CORRECT — Log a mood through MoodLogger (handles all side effects) +MoodLogger.shared.logMood(.great, for: Date(), entryType: .listView) + +// ✅ CORRECT — Read data through DataController +let entry = DataController.shared.getEntry(byDate: date) +let entries = DataController.shared.getData(startDate: start, endDate: end, includedDays: [1,2,3,4,5,6,7]) + +// ✅ CORRECT — Track analytics through AnalyticsManager +AnalyticsManager.shared.track(.moodLogged(mood: mood.rawValue, entryType: "listView")) +``` + +### Wrong Usage + +```swift +// ❌ WRONG — Never insert into modelContext directly (bypasses side effects and analytics) +let entry = MoodEntryModel(forDate: date, mood: .great, entryType: .listView) +modelContext.insert(entry) + +// ❌ WRONG — Never call PostHogSDK directly (bypasses opt-out checks and config) +PostHogSDK.shared.capture("mood_logged") + +// ❌ WRONG — Never create a separate ModelContainer (breaks shared state) +let container = try ModelContainer(for: MoodEntryModel.self) +``` + +### Allowed Exceptions + +- `DataControllerADD.swift` — Directly inserts into `modelContext` (this IS the data layer) +- `ExtensionDataProvider` — Creates its own `ModelContainer` for widget/watch (can't share process) +- Widget extensions — May call `DataController.add()` directly when `MoodLogger` isn't available + +## Architecture Rules + +- All mood logging MUST go through `MoodLogger.shared.logMood()`. This ensures HealthKit sync, streak calculation, widget refresh, watch notification, and analytics all fire. +- All data reads MUST go through `DataController.shared` methods (`getEntry`, `getData`, `splitIntoYearMonth`). Views MUST NOT use `@Query` or direct `modelContext` access. +- All analytics MUST go through `AnalyticsManager.shared`. NEVER call `PostHogSDK` directly. +- Any code that modifies mood data MUST call `WidgetCenter.shared.reloadAllTimelines()` (handled automatically by `MoodLogger`). +- NEVER access `DataController.shared` from a background thread — it is `@MainActor` isolated. +- NEVER hardcode App Group identifiers — use `Constants.groupShareId` / `Constants.groupShareIdDebug` or `SharedModelContainer.appGroupID`. +- NEVER hardcode CloudKit container IDs — use `SharedModelContainer.cloudKitContainerID`. +- New SwiftData fetch operations MUST be added to `DataControllerGET.swift`. +- New create operations MUST be added to `DataControllerADD.swift`. +- New update operations MUST be added to `DataControllerUPDATE.swift`. +- New delete operations MUST be added to `DataControllerDELETE.swift`. +- ViewModels MUST be `@MainActor class` conforming to `ObservableObject`. +- ALWAYS use `@StateObject` (not `@ObservedObject`) when a view owns its ViewModel. +- NEVER bypass `IAPManager` subscription checks — use `IAPManager.shared.shouldShowPaywall` to gate premium features. + +## Mutation / Write Patterns + +``` +Create: MoodLogger.shared.logMood(mood, for: date, entryType: type) + → DataController.shared.add(mood:forDate:entryType:) + → Deletes existing entries for that date first (prevents duplicates) + → Inserts new MoodEntryModel + → saveAndRunDataListeners() + +Update: DataController.shared.update(entryDate:withMood:) + DataController.shared.updateNotes(forDate:notes:) + DataController.shared.updatePhoto(forDate:photoID:) + +Delete: DataController.shared.clearDB() + DataController.shared.deleteLast(numberOfEntries:) + +Fill: DataController.shared.fillInMissingDates() + → Called by BGTask to backfill missing days with .missing entries +``` + +### Correct Mutation + +```swift +// ✅ CORRECT — Create a mood entry (full flow with side effects) +MoodLogger.shared.logMood(.good, for: selectedDate, entryType: .listView) + +// ✅ CORRECT — Update notes on an existing entry +DataController.shared.updateNotes(forDate: date, notes: "Had a great day") +DataController.shared.saveAndRunDataListeners() +``` + +### Wrong Mutation + +```swift +// ❌ WRONG — Calling DataController.add() directly skips side effects +DataController.shared.add(mood: .good, forDate: date, entryType: .listView) +// Missing: HealthKit sync, streak calc, widget reload, watch notify, analytics +``` + +## Concurrency Patterns + +- `DataController` is `@MainActor final class` — all SwiftData operations run on main actor. +- `MoodLogger` is `@MainActor final class` — mood logging and side effects run on main actor. +- `AnalyticsManager` is `@MainActor final class` — analytics calls run on main actor. +- `IAPManager` is `@MainActor class ObservableObject` — StoreKit state is main-actor bound. +- `ExtensionDataProvider` is `@MainActor final class` — extension data access on main actor. +- `HealthKitManager` is `class ObservableObject` (NOT @MainActor) — HealthKit calls may be async. +- `WatchConnectivityManager` is `NSObject, ObservableObject` — WCSession delegate callbacks. +- All ViewModels (`DayViewViewModel`, `YearViewModel`, `InsightsViewModel`) are `@MainActor class ObservableObject`. +- Background work (HealthKit sync) MUST use `Task { }` from `@MainActor` context. +- CloudKit sync happens automatically via SwiftData's built-in CloudKit integration — no manual threading. +- `BGTask.swift` runs `fillInMissingDates()` which accesses `DataController.shared` on the main actor. + +### Swift 6 / Sendable Notes + +- `Color` gets `@retroactive Codable` and `@retroactive RawRepresentable` conformance in `Color+Codable.swift`. +- `Date` gets `@retroactive RawRepresentable` conformance in `Date+Extensions.swift`. +- Widget `Provider` uses `@preconcurrency IntentTimelineProvider` to suppress Sendable warnings in `WidgetProviders.swift`. +- `AppDelegate` uses `@preconcurrency UNUserNotificationCenterDelegate` for notification delegate callbacks. +- `WatchConnectivityManager` is `NSObject, ObservableObject` — WCSession delegate callbacks arrive on arbitrary threads. Ensure UI updates dispatch to main actor. +- `HealthKitManager` is NOT `@MainActor` — its async methods may cross isolation boundaries when called from `@MainActor` ViewModels. + +## State Management + +### Cache / Offline Behavior + +- SwiftData persists all mood data locally in the App Group shared container. +- CloudKit sync is automatic — works offline and syncs when connectivity returns. +- No explicit cache layer — SwiftData IS the cache and persistence. +- Widget reads from the same shared App Group container (local file, not CloudKit). +- Watch uses CloudKit for data sync (separate from WCSession which is for UI updates only). +- If CloudKit sync fails, local data is always available. + +### Startup Flow + +1. `FeelsApp.init()` — Configure `AnalyticsManager`, register `BGTaskScheduler`, reset `FeelsTipsManager`, initialize `LiveActivityScheduler`, initialize `WatchConnectivityManager` +2. `MainTabView` receives `DataController.shared.container` as `modelContainer` +3. `IAPManager`, `BiometricAuthManager`, `HealthKitManager` injected as `@EnvironmentObject` +4. On `scenePhase` change to `.active` — `DataController.shared.refreshFromDisk()` picks up widget/watch changes +5. On `scenePhase` change to `.background` — schedule `BGTask` for missing date backfill + +## Test Conventions + +### Framework & Location + +- **Framework**: XCTest +- **Test directory**: `Tests iOS/` (iOS), `Tests macOS/` (macOS — template only) +- **File naming**: `{SuiteName}Tests.swift` + +### Existing Test Suites + +| Suite | Test Count | Covers | +|-------|-----------|--------| +| `Tests_iOS` | 2 | `Date.dates(from:toDate:)` utility — basic date range generation | +| `Tests_iOSLaunchTests` | 1 | Default launch test (template) | + +**Note**: Test coverage is minimal. Most of the app is untested. Priority areas for new tests: `DataController` CRUD operations, `MoodLogger` side effects, `IAPManager` subscription state transitions, `MoodEntryModel` initialization edge cases. + +### Naming Convention + +``` +test{Component}_{Behavior} +``` + +Example: `testDatesBetween`, `testDatesIncluding` + +### Mocking Strategy + +- **SwiftData**: Use `ModelContainer` with `isStoredInMemoryOnly: true` for test isolation +- **DataController**: The `DataControllerProtocol.swift` defines `MoodDataReading`, `MoodDataWriting`, `MoodDataDeleting`, `MoodDataPersisting` protocols — use these for protocol-based mocking +- **Analytics**: No mock needed — `AnalyticsManager` can be stubbed or ignored in tests +- **HealthKit**: Mock `HKHealthStore` or skip — not critical for unit tests +- **StoreKit**: Use StoreKit Testing in Xcode for `IAPManager` tests + +Example: +```swift +// Test setup with in-memory SwiftData +let schema = Schema([MoodEntryModel.self]) +let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true) +let container = try ModelContainer(for: schema, configurations: [config]) +let context = container.mainContext +``` + +### Bug Fix Protocol + +When fixing a bug: +1. Write a regression test that reproduces the bug BEFORE fixing it +2. Include edge cases — test boundary conditions, nil/empty inputs, related scenarios +3. Confirm all tests pass after the fix +4. Name tests descriptively: `test{Component}_{WhatWasBroken}` + +## Known Edge Cases & Gotchas + +### Platform Gotchas + +- Widget extensions cannot access CloudKit — they use local App Group storage via `ExtensionDataProvider` +- Widget extensions cannot access HealthKit or TipKit — `MoodLogger.logMood()` has `syncHealthKit` and `updateTips` flags for this +- watchOS uses CloudKit for data sync (automatic), WCSession only for UI update notifications + +### Framework Gotchas + +- SwiftData with CloudKit requires all `@Model` properties to have default values (CloudKit requirement) +- SwiftData `@Model` stores enums as raw `Int` values — `moodValue: Int` not `mood: Mood` directly +- `DataController.add()` deletes ALL existing entries for a date before inserting — this prevents duplicates but means "update" is really "delete + insert" +- `modelContext.rollback()` is used in `refreshFromDisk()` to pick up extension changes — this discards any unsaved in-process changes +- `SharedModelContainer.createWithFallback()` silently falls back to in-memory storage if App Group container fails — data loss risk if App Group is misconfigured + +### Data Gotchas + +- **Two entries for same date**: Handled by `add()` which deletes existing entries first, but could occur via CloudKit sync conflict +- **Mood value outside enum range**: `Mood(rawValue:)` returns `nil`, caught by `?? .missing` fallback in `MoodEntryModel.mood` +- **Timezone change crossing midnight**: `forDate` is stored as `Date` — `Calendar.current.startOfDay(for:)` used for comparisons, but timezone changes could shift which "day" an entry belongs to +- **Missing dates backfill**: `fillInMissingDates()` creates `.missing` entries for gaps — if it runs during timezone change, could create entries for wrong dates +- **Widget timeline with 0 entries**: `ExtensionDataProvider` returns empty array — widget must handle empty state +- **CloudKit sync conflict**: SwiftData/CloudKit uses last-writer-wins — no custom conflict resolution + +### Common Developer Mistakes + +- Calling `DataController.shared.add()` directly instead of `MoodLogger.shared.logMood()` — skips all side effects +- Creating a new `ModelContainer` instead of using `DataController.shared.container` — breaks shared state +- Accessing `DataController` from a background thread — it's `@MainActor`, will crash or produce undefined behavior +- Hardcoding App Group ID strings instead of using `Constants` or `SharedModelContainer.appGroupID` +- Forgetting to call `saveAndRunDataListeners()` after mutations — listeners won't fire, UI won't update +- Using `@ObservedObject` instead of `@StateObject` for ViewModels the view owns — causes recreation on every view update + +## External Boundaries + +| Boundary | Handler Class | Input Source | What Could Go Wrong | +|----------|--------------|-------------|---------------------| +| CloudKit sync | `SharedModelContainer` (automatic) | iCloud | Sync conflicts (last-writer-wins), offline delays, iCloud account not signed in | +| StoreKit 2 | `IAPManager` | App Store | Network timeout during purchase, receipt validation failure, subscription state mismatch | +| HealthKit | `HealthKitManager` | Health app | Permission denied, Health data unavailable, write conflicts | +| App Group storage | `SharedModelContainer`, `ExtensionDataProvider` | File system | Container URL nil (entitlement misconfigured), file corruption | +| Watch Connectivity | `WatchConnectivityManager` | WCSession | Watch not paired, session not reachable, message delivery failure | +| Background Tasks | `BGTask` | System scheduler | System may not run task, task killed by system, insufficient time | +| Apple Foundation Models | `FoundationModelsInsightService` | On-device AI | Model not available on device, generation failure, unexpected output | +| PostHog | `AnalyticsManager` | Network | analytics.88oakapps.com unreachable, event queue overflow | +| Deep Links | `FeelsApp.onOpenURL` | URL scheme `feels://` | Malformed URL, unknown host/path | +| App Shortcuts / Siri | `AppShortcuts.swift` | SiriKit | Intent not recognized, missing parameter | + +## Analytics + +- **SDK**: PostHog (self-hosted at `analytics.88oakapps.com`) +- **Manager**: `AnalyticsManager.shared` in `Shared/Analytics.swift` — NEVER call `PostHogSDK` directly +- **Events**: Defined as `AnalyticsManager.Event` enum in `Analytics.swift` +- **Screens**: Defined as `AnalyticsManager.Screen` enum in `Analytics.swift` +- **Opt-out**: User can opt out via settings — `AnalyticsManager.isOptedOut` +- **Session Replay**: Supported, toggled via `AnalyticsManager.sessionReplayEnabled` + +**Adding new analytics:** +```swift +// 1. Add case to AnalyticsManager.Event enum in Analytics.swift +case myNewEvent(param: String) + +// 2. Add name and properties in the `payload` computed property switch +case .myNewEvent(let param): + return ("my_new_event", ["param": param]) + +// 3. Call from anywhere: +AnalyticsManager.shared.track(.myNewEvent(param: "value")) + +// For screen tracking: +AnalyticsManager.shared.trackScreen(.day) +``` + +## Environment Configuration + +- **Debug**: Uses `iCloud.com.88oakapps.feels.debug` CloudKit container, `group.com.88oakapps.feels.debug` App Group, `Feels-Debug.store` filename +- **Production**: Uses `iCloud.com.88oakapps.feels` CloudKit container, `group.com.88oakapps.feels` App Group, `Feels.store` filename +- **Toggle**: `#if DEBUG` preprocessor directive in `SharedModelContainer` and throughout codebase +- **StoreKit Testing**: `IAPManager.bypassSubscription` flag (DEBUG only) — set to `true` to bypass paywall during development +- **Subscription Group ID**: `21914363` +- **Product IDs**: `com.88oakapps.feels.IAP.subscriptions.monthly`, `com.88oakapps.feels.IAP.subscriptions.yearly` +- **Trial**: 30-day free trial tracked via `firstLaunchDate` in `GroupUserDefaults` + +## Directory Conventions + +When adding new files: +- New views: `Shared/Views/{FeatureName}/` +- New view models: Colocated with views in `Shared/Views/{FeatureName}/` +- New models: `Shared/Models/` +- New services/managers: `Shared/Services/` +- New persistence operations: `Shared/Persisence/DataController{OPERATION}.swift` (note directory typo) +- New widget code: `FeelsWidget2/` +- New watch code: `Feels Watch App/` +- New tests: `Tests iOS/` + +### Naming Conventions + +- **ViewModels**: `{Feature}ViewModel.swift` — e.g., `DayViewViewModel.swift`, `YearViewModel.swift`, `InsightsViewModel.swift` +- **Views**: `{Feature}View.swift` — e.g., `DayView.swift`, `MonthView.swift`, `YearView.swift` +- **Models**: `{ModelName}Model.swift` for SwiftData models — e.g., `MoodEntryModel.swift` +- **Services/Managers**: `{Purpose}Manager.swift` — e.g., `IAPManager.swift`, `HealthKitManager.swift`, `PhotoManager.swift` +- **DataController operations**: `DataController{OPERATION}.swift` — e.g., `DataControllerGET.swift`, `DataControllerADD.swift` +- **Tests**: `{SuiteName}Tests.swift` — e.g., `Tests_iOS.swift` + +## Localization + +- English: `en.lproj/Localizable.strings` +- Spanish: `es.lproj/Localizable.strings` +- Use `String(localized:)` for all user-facing strings + +## Dependencies + +### Package Manager + +- No external SPM packages (pure Apple frameworks) +- Single external dependency: **PostHog** iOS SDK (added via SPM or directly) + +### Key Frameworks + +| Framework | Purpose | +|-----------|---------| +| SwiftData | Persistence and CloudKit sync | +| CloudKit | Automatic cloud sync via SwiftData | +| StoreKit 2 | Subscriptions and in-app purchases | +| HealthKit | Mood data sync to Health app | +| WidgetKit | Home screen and Lock Screen widgets | +| ActivityKit | Live Activities for mood streaks | +| BackgroundTasks | BGProcessingTask for missing date backfill | +| WatchConnectivity | Phone-watch communication | +| LocalAuthentication | Biometric auth (Face ID / Touch ID) | +| PostHog | Analytics and session replay | +| Foundation Models | On-device AI for mood insights | + ## Mood Values ```swift @@ -52,19 +435,24 @@ enum Mood: Int { case average = 2 case good = 3 case great = 4 - case missing = 5 // Unfilled day - case placeholder = 6 // System-generated + case missing = 5 // Unfilled day (system-generated) + case placeholder = 6 // Calendar padding (system-generated) } ``` -## Localization +## Entry Types -- English: `en.lproj/Localizable.strings` -- Spanish: `es.lproj/Localizable.strings` - -## Important Patterns - -1. **Widgets** update via `WidgetCenter.shared.reloadAllTimelines()` -2. **Missing dates** are auto-filled by background task (`BGTask.swift`) -3. **Entry types** distinguish user entries from system-generated ones -4. **Customization** uses protocols: `Themeable`, `MoodTintable`, `MoodImagable`, `PersonalityPackable` +```swift +enum EntryType: Int, Codable { + case listView = 0 // Main app day view + case widget = 1 // Home screen widget + case watch = 2 // watchOS app + case shortcut = 3 // Shortcuts app + case filledInMissing = 4 // BGTask backfill + case notification = 5 // Push notification + case header = 6 // Header quick-entry + case siri = 7 // Siri / App Intent + case controlCenter = 8 // Control Center widget + case liveActivity = 9 // Live Activity +} +```