diff --git a/Feels/Localizable.xcstrings b/Feels/Localizable.xcstrings index 0aa0160..9800328 100644 --- a/Feels/Localizable.xcstrings +++ b/Feels/Localizable.xcstrings @@ -891,6 +891,10 @@ } } }, + "Add Test Data" : { + "comment" : "A button label that adds test data to the app.", + "isCommentAutoGenerated" : true + }, "Add the Mood Vote widget to quickly log your mood without opening the app." : { "comment" : "A message encouraging users to add the Mood Vote widget to their home screen to log moods without using the main app.", "isCommentAutoGenerated" : true, @@ -1829,6 +1833,10 @@ "comment" : "A description of the benefits of the premium subscription.", "isCommentAutoGenerated" : true }, + "Clear All Data" : { + "comment" : "A button label that clears all data from the app.", + "isCommentAutoGenerated" : true + }, "Clear DB" : { "comment" : "A button label that clears the app's database.", "isCommentAutoGenerated" : true, @@ -4200,6 +4208,10 @@ } } }, + "Delete all mood entries" : { + "comment" : "A description of what the \"Clear All Data\" button does.", + "isCommentAutoGenerated" : true + }, "Delete Entry" : { "comment" : "An alert that appears when the user confirms they want to delete an entry.", "isCommentAutoGenerated" : true, @@ -9038,6 +9050,10 @@ } } }, + "Populate with sample mood entries" : { + "comment" : "A description of what the \"Add Test Data\" button does.", + "isCommentAutoGenerated" : true + }, "Predict your patterns.\nPrepare for any weather." : { "comment" : "A description of the premium feature, \"Your Emotional Forecast\", that appears below the title.", "isCommentAutoGenerated" : true diff --git a/Shared/FeelsTips.swift b/Shared/FeelsTips.swift index d476959..fa7590b 100644 --- a/Shared/FeelsTips.swift +++ b/Shared/FeelsTips.swift @@ -150,28 +150,6 @@ struct MoodStreakTip: Tip { static var currentStreak: Int = 0 } -/// Tip for Control Center widget -struct ControlCenterTip: Tip { - var title: Text { - Text("Quick Access from Control Center") - } - - var message: Text? { - Text("Add Feels to Control Center for one-tap mood logging from anywhere.") - } - - var image: Image? { - Image(systemName: "slider.horizontal.3") - } - - var rules: [Rule] { - #Rule(Self.$daysUsingApp) { $0 >= 5 } - } - - @Parameter - static var daysUsingApp: Int = 0 -} - // MARK: - Tips Manager @MainActor @@ -203,7 +181,6 @@ class TipsManager { func updateDaysUsingApp(_ days: Int) { WidgetVotingTip.daysUsingApp = days - ControlCenterTip.daysUsingApp = days } func updateStreak(_ streak: Int) { @@ -241,10 +218,6 @@ extension View { func moodStreakTip() -> some View { self.popoverTip(MoodStreakTip()) } - - func controlCenterTip() -> some View { - self.popoverTip(ControlCenterTip()) - } } // MARK: - Inline Tip View diff --git a/Shared/Views/AddMoodHeaderView.swift b/Shared/Views/AddMoodHeaderView.swift index 9b5f8c4..eff2aa4 100644 --- a/Shared/Views/AddMoodHeaderView.swift +++ b/Shared/Views/AddMoodHeaderView.swift @@ -382,6 +382,9 @@ struct OrbitVotingView: View { centerPulse = 1.1 } } + .onDisappear { + centerPulse = 1.0 + } .accessibilityElement(children: .contain) .accessibilityLabel(String(localized: "Mood selection")) } @@ -589,6 +592,9 @@ struct NeonVotingView: View { pulsePhase = true } } + .onDisappear { + pulsePhase = false + } } private var neonGridBackground: some View { diff --git a/Shared/Views/FeelsSubscriptionStoreView.swift b/Shared/Views/FeelsSubscriptionStoreView.swift index 80fca65..59de0ed 100644 --- a/Shared/Views/FeelsSubscriptionStoreView.swift +++ b/Shared/Views/FeelsSubscriptionStoreView.swift @@ -151,6 +151,10 @@ struct CelestialMarketingContent: View { showContent = true } } + .onDisappear { + animateGradient = false + animateOrbs = false + } } } @@ -221,6 +225,10 @@ struct GardenMarketingContent: View { showContent = true } } + .onDisappear { + bloomPhase = false + swayPhase = false + } } } @@ -299,6 +307,11 @@ struct NeonMarketingContent: View { showContent = true } } + .onDisappear { + pulsePhase = false + glowPhase = false + scanlineOffset = 0 + } } } @@ -358,6 +371,9 @@ struct MinimalMarketingContent: View { showContent = true } } + .onDisappear { + breathe = false + } } } @@ -415,6 +431,9 @@ struct ZenMarketingContent: View { showContent = true } } + .onDisappear { + inkFlow = false + } } } @@ -548,6 +567,9 @@ struct MixtapeMarketingContent: View { showContent = true } } + .onDisappear { + tapeRotation = false + } } } @@ -612,6 +634,9 @@ struct HeartfeltMarketingContent: View { showContent = true } } + .onDisappear { + heartbeat = false + } } } @@ -674,6 +699,9 @@ struct LuxeMarketingContent: View { showContent = true } } + .onDisappear { + shimmer = false + } } } @@ -738,6 +766,9 @@ struct ForecastMarketingContent: View { showContent = true } } + .onDisappear { + cloudDrift = false + } } } @@ -802,6 +833,9 @@ struct PlayfulMarketingContent: View { showContent = true } } + .onDisappear { + bounce = false + } } } @@ -858,6 +892,9 @@ struct JournalMarketingContent: View { showContent = true } } + .onDisappear { + pageFlip = false + } } } @@ -1452,6 +1489,9 @@ struct EmotionOrb: View { pulse = true } } + .onDisappear { + pulse = false + } } } diff --git a/Shared/Views/LockScreenView.swift b/Shared/Views/LockScreenView.swift index ea214a3..eb47c3a 100644 --- a/Shared/Views/LockScreenView.swift +++ b/Shared/Views/LockScreenView.swift @@ -118,22 +118,6 @@ struct AuroraBackground: View { .blur(radius: 40) .offset(y: animateGradient ? -10 : 10) - // Noise texture overlay - Rectangle() - .fill((isDark ? Color.white : Color.black).opacity(0.015)) - .background( - Canvas { context, size in - for _ in 0..<1000 { - let x = CGFloat.random(in: 0...size.width) - let y = CGFloat.random(in: 0...size.height) - let opacity = Double.random(in: 0.01...0.04) - context.fill( - Path(ellipseIn: CGRect(x: x, y: y, width: 1, height: 1)), - with: .color((isDark ? Color.white : Color.black).opacity(opacity)) - ) - } - } - ) } .ignoresSafeArea() .onAppear { @@ -365,6 +349,9 @@ struct ZenLockBackground: View { breathe = true } } + .onDisappear { + breathe = false + } } } @@ -401,6 +388,9 @@ struct ZenEnsoOrb: View { breathe = true } } + .onDisappear { + breathe = false + } } } @@ -450,6 +440,9 @@ struct NeonLockBackground: View { pulse = true } } + .onDisappear { + pulse = false + } } } @@ -501,6 +494,10 @@ struct NeonRingOrb: View { pulse = true } } + .onDisappear { + rotate = false + pulse = false + } } } @@ -560,6 +557,9 @@ struct CelestialLockBackground: View { twinkle = true } } + .onDisappear { + twinkle = false + } } } @@ -620,6 +620,10 @@ struct CelestialOrbsElement: View { float = true } } + .onDisappear { + rotate = false + float = false + } } } @@ -704,21 +708,6 @@ struct MixtapeLockBackground: View { endPoint: shift ? .bottomTrailing : .bottomLeading ) - // Noise texture - Rectangle() - .fill(.white.opacity(0.03)) - .background( - Canvas { context, size in - for _ in 0..<500 { - let x = CGFloat.random(in: 0...size.width) - let y = CGFloat.random(in: 0...size.height) - context.fill( - Path(ellipseIn: CGRect(x: x, y: y, width: 1, height: 1)), - with: .color(.white.opacity(Double.random(in: 0.02...0.06))) - ) - } - } - ) } .ignoresSafeArea() .onAppear { @@ -726,6 +715,9 @@ struct MixtapeLockBackground: View { shift = true } } + .onDisappear { + shift = false + } } } @@ -775,6 +767,9 @@ struct CassetteElement: View { spin = true } } + .onDisappear { + spin = false + } } } @@ -818,6 +813,9 @@ struct BloomLockBackground: View { bloom = true } } + .onDisappear { + bloom = false + } } } @@ -867,6 +865,9 @@ struct FlowerElement: View { bloom = true } } + .onDisappear { + bloom = false + } } } @@ -904,6 +905,9 @@ struct HeartfeltLockBackground: View { pulse = true } } + .onDisappear { + pulse = false + } } } @@ -940,6 +944,9 @@ struct HeartElement: View { beat = true } } + .onDisappear { + beat = false + } } } @@ -1000,6 +1007,9 @@ struct MinimalCircleElement: View { breathe = true } } + .onDisappear { + breathe = false + } } } @@ -1037,6 +1047,9 @@ struct LuxeLockBackground: View { shimmer = true } } + .onDisappear { + shimmer = false + } } } @@ -1078,6 +1091,10 @@ struct DiamondElement: View { shimmer = true } } + .onDisappear { + rotate = false + shimmer = false + } } } @@ -1117,6 +1134,9 @@ struct ForecastLockBackground: View { drift = true } } + .onDisappear { + drift = false + } } } @@ -1169,6 +1189,9 @@ struct WeatherElement: View { shine = true } } + .onDisappear { + shine = false + } } } @@ -1235,6 +1258,10 @@ struct PlayfulEmojiElement: View { bounce = true } } + .onDisappear { + wiggle = false + bounce = false + } } } @@ -1326,6 +1353,9 @@ struct JournalBookElement: View { open = true } } + .onDisappear { + open = false + } } } @@ -1863,6 +1893,9 @@ struct NeonUnlockButton: View { pulse = true } } + .onDisappear { + pulse = false + } } } diff --git a/Shared/Views/SettingsView/PaywallPreviewSettingsView.swift b/Shared/Views/SettingsView/PaywallPreviewSettingsView.swift index 39ec507..94fd63f 100644 --- a/Shared/Views/SettingsView/PaywallPreviewSettingsView.swift +++ b/Shared/Views/SettingsView/PaywallPreviewSettingsView.swift @@ -386,6 +386,9 @@ struct CelestialMiniPreview: View { animate = true } } + .onDisappear { + animate = false + } } private var orbColors: [Color] { @@ -445,6 +448,9 @@ struct GardenMiniPreview: View { bloom = true } } + .onDisappear { + bloom = false + } } } @@ -508,6 +514,9 @@ struct NeonMiniPreview: View { pulse = true } } + .onDisappear { + pulse = false + } } } @@ -552,6 +561,9 @@ struct MinimalMiniPreview: View { breathe = true } } + .onDisappear { + breathe = false + } } } @@ -592,6 +604,9 @@ struct ZenMiniPreview: View { breathe = true } } + .onDisappear { + breathe = false + } } } @@ -668,6 +683,9 @@ struct MixtapeMiniPreview: View { spin = true } } + .onDisappear { + spin = false + } } } @@ -710,6 +728,9 @@ struct HeartfeltMiniPreview: View { beat = true } } + .onDisappear { + beat = false + } } } @@ -756,6 +777,9 @@ struct LuxeMiniPreview: View { shimmer = true } } + .onDisappear { + shimmer = false + } } } @@ -801,6 +825,9 @@ struct ForecastMiniPreview: View { drift = true } } + .onDisappear { + drift = false + } } } @@ -847,6 +874,9 @@ struct PlayfulMiniPreview: View { bounce = true } } + .onDisappear { + bounce = false + } } } diff --git a/Shared/Views/SettingsView/SettingsView.swift b/Shared/Views/SettingsView/SettingsView.swift index b89607d..ad7d414 100644 --- a/Shared/Views/SettingsView/SettingsView.swift +++ b/Shared/Views/SettingsView/SettingsView.swift @@ -55,6 +55,8 @@ struct SettingsContentView: View { trialDateButton animationLabButton paywallPreviewButton + addTestDataButton + clearDataButton #endif Spacer() @@ -159,7 +161,6 @@ struct SettingsContentView: View { } .padding(.top, 20) .padding(.horizontal, 4) - .controlCenterTip() } private var legalSectionHeader: some View { @@ -331,6 +332,66 @@ struct SettingsContentView: View { } } } + + private var addTestDataButton: some View { + ZStack { + theme.currentTheme.secondaryBGColor + Button { + DataController.shared.populateTestData() + } label: { + HStack(spacing: 12) { + Image(systemName: "plus.square.on.square") + .font(.title2) + .foregroundColor(.green) + .frame(width: 32) + + VStack(alignment: .leading, spacing: 2) { + Text("Add Test Data") + .foregroundColor(textColor) + + Text("Populate with sample mood entries") + .font(.caption) + .foregroundStyle(.secondary) + } + + Spacer() + } + .padding() + } + } + .fixedSize(horizontal: false, vertical: true) + .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) + } + + private var clearDataButton: some View { + ZStack { + theme.currentTheme.secondaryBGColor + Button { + DataController.shared.clearDB() + } label: { + HStack(spacing: 12) { + Image(systemName: "trash") + .font(.title2) + .foregroundColor(.red) + .frame(width: 32) + + VStack(alignment: .leading, spacing: 2) { + Text("Clear All Data") + .foregroundColor(textColor) + + Text("Delete all mood entries") + .font(.caption) + .foregroundStyle(.secondary) + } + + Spacer() + } + .padding() + } + } + .fixedSize(horizontal: false, vertical: true) + .cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight]) + } #endif // MARK: - Privacy Lock Toggle diff --git a/docs/Apple-Features.md b/docs/Apple-Features.md index 8069fed..c533c8c 100644 --- a/docs/Apple-Features.md +++ b/docs/Apple-Features.md @@ -126,7 +126,6 @@ This document covers the new Apple-specific features integrated into Feels, incl | WidgetVotingTip | Day view | After first mood log | | TimeViewTip | Day view header | After 2 days usage | | MoodStreakTip | Day view | When streak >= 3 | -| ControlCenterTip | Settings | After 10 mood logs | ### How to Test 1. Tips appear automatically based on conditions diff --git a/docs/TipKit-Tips.md b/docs/TipKit-Tips.md new file mode 100644 index 0000000..24d66fd --- /dev/null +++ b/docs/TipKit-Tips.md @@ -0,0 +1,152 @@ +# TipKit Tips Documentation + +This document describes all TipKit tips implemented in the Feels app, including their display conditions and locations. + +## Overview + +Tips are managed by `TipsManager` (singleton) and configured with: +- **Display Frequency**: Daily +- **Datastore Location**: Application default + +--- + +## Tips + +### 1. CustomizeLayoutTip + +**Title**: "Personalize Your Experience" +**Message**: "Tap here to customize mood icons, colors, and layouts." +**Icon**: `paintbrush` + +**Display Conditions**: Always eligible (no rules) + +**Location**: CustomizeContentView (top of the Customize tab in Settings) + +--- + +### 2. AIInsightsTip + +**Title**: "Discover AI Insights" +**Message**: "Get personalized insights about your mood patterns powered by Apple Intelligence." +**Icon**: `brain` + +**Display Conditions**: +- User has logged at least **7 moods** + +**Parameter**: `hasLoggedMoods: Int` (incremented via `TipsManager.shared.onMoodLogged()`) + +**Location**: InsightsView + +--- + +### 3. SiriShortcutTip + +**Title**: "Use Siri to Log Moods" +**Message**: "Say 'Hey Siri, log my mood as great in Feels' for hands-free logging." +**Icon**: `mic.fill` + +**Display Conditions**: +- User has logged at least **3 moods** + +**Parameter**: `moodLogCount: Int` (incremented via `TipsManager.shared.onMoodLogged()`) + +**Location**: SettingsContentView (Features section header, via `.siriShortcutTip()`) + +--- + +### 4. HealthKitSyncTip + +**Title**: "Sync with Apple Health" +**Message**: "Connect to Apple Health to see your mood data alongside sleep, exercise, and more." +**Icon**: `heart.fill` + +**Display Conditions**: +- User has viewed the Settings screen + +**Parameter**: `hasSeenSettings: Bool` (set via `TipsManager.shared.onSettingsViewed()`) + +**Location**: SettingsContentView (Health Kit toggle, via `.healthKitSyncTip()`) + +--- + +### 5. WidgetVotingTip + +**Title**: "Vote from Your Home Screen" +**Message**: "Add the Mood Vote widget to quickly log your mood without opening the app." +**Icon**: `square.grid.2x2` + +**Display Conditions**: +- User has been using the app for at least **2 days** + +**Parameter**: `daysUsingApp: Int` (updated via `TipsManager.shared.updateDaysUsingApp(_:)`) + +**Location**: DayView + +--- + +### 6. TimeViewTip + +**Title**: "View Your History" +**Message**: "Switch between Day, Month, and Year views to see your mood patterns over time." +**Icon**: `calendar` + +**Display Conditions**: Always eligible (no rules) + +**Location**: DayView + +--- + +### 7. MoodStreakTip + +**Title**: "Build Your Streak!" +**Message**: "Log your mood daily to build a streak. Consistency helps you understand your patterns." +**Icon**: `flame.fill` + +**Display Conditions**: +- User has a current streak of at least **3 days** + +**Parameter**: `currentStreak: Int` (updated via `TipsManager.shared.updateStreak(_:)`) + +**Location**: DayView + +--- + +## TipsManager API + +```swift +// Configure tips (call on app launch) +TipsManager.shared.configure() + +// Reset all tips (for testing) +TipsManager.shared.resetAllTips() + +// Update parameters +TipsManager.shared.onMoodLogged() // Increments mood log count +TipsManager.shared.onSettingsViewed() // Marks settings as viewed +TipsManager.shared.updateDaysUsingApp(_:) // Updates days using app +TipsManager.shared.updateStreak(_:) // Updates current streak +``` + +--- + +## View Modifiers + +Tips can be attached to views using these convenience modifiers: + +```swift +.customizeLayoutTip() +.aiInsightsTip() +.siriShortcutTip() +.healthKitSyncTip() +.widgetVotingTip() +.timeViewTip() +.moodStreakTip() +``` + +--- + +## Files + +- **Definition**: `Shared/FeelsTips.swift` +- **Manager**: `TipsManager` class in same file +- **Configuration**: Called in `FeelsApp.swift`