Add weather feature with WeatherKit integration for mood entries

Fetch and display weather data (temp, condition, hi/lo, humidity) when
users log a mood. Weather is stored as JSON on MoodEntryModel and shown
as a card in EntryDetailView. Premium-gated with location permission
prompt. Includes BGTask retry for failed fetches and full analytics.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-03-11 00:16:26 -05:00
parent a1340b4deb
commit 31fb2a7fe2
15 changed files with 557 additions and 3 deletions

View File

@@ -39,6 +39,8 @@ struct SettingsContentView: View {
@AppStorage(UserDefaultsStore.Keys.deleteEnable.rawValue, store: GroupUserDefaults.groupDefaults) private var deleteEnabled = true
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
@AppStorage(UserDefaultsStore.Keys.hapticFeedbackEnabled.rawValue, store: GroupUserDefaults.groupDefaults) private var hapticFeedbackEnabled = true
@AppStorage(UserDefaultsStore.Keys.weatherEnabled.rawValue, store: GroupUserDefaults.groupDefaults) private var weatherEnabled = true
@State private var showWeatherSubscriptionStore = false
private var textColor: Color { theme.currentTheme.labelColor }
@@ -49,6 +51,7 @@ struct SettingsContentView: View {
featuresSectionHeader
privacyLockToggle
healthKitToggle
weatherToggle
exportDataButton
// Settings section
@@ -959,6 +962,72 @@ struct SettingsContentView: View {
}
}
// MARK: - Weather Toggle
private var weatherToggle: some View {
VStack(spacing: 0) {
HStack(spacing: 12) {
Image(systemName: "cloud.sun.fill")
.font(.title2)
.foregroundColor(.blue)
.frame(width: 32)
VStack(alignment: .leading, spacing: 2) {
Text("Weather")
.foregroundColor(textColor)
Text("Show weather details for each day")
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
Toggle("", isOn: Binding(
get: { iapManager.shouldShowPaywall ? false : weatherEnabled },
set: { newValue in
if iapManager.shouldShowPaywall {
showWeatherSubscriptionStore = true
return
}
weatherEnabled = newValue
if newValue {
LocationManager.shared.requestAuthorization()
}
AnalyticsManager.shared.track(.weatherToggled(enabled: newValue))
}
))
.labelsHidden()
.disabled(iapManager.shouldShowPaywall)
.accessibilityLabel(String(localized: "Weather"))
.accessibilityHint(String(localized: "Show weather details for each day"))
}
.padding()
if iapManager.shouldShowPaywall {
HStack {
Image(systemName: "crown.fill")
.foregroundColor(.yellow)
Text("Premium Feature")
.font(.caption)
.foregroundStyle(.secondary)
}
.padding(.horizontal)
.padding(.bottom, 12)
.accessibilityElement(children: .combine)
.accessibilityLabel(String(localized: "Premium feature, subscription required"))
}
}
.background(theme.currentTheme.secondaryBGColor)
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
.sheet(isPresented: $showWeatherSubscriptionStore) {
ReflectSubscriptionStoreView(source: "settings")
}
}
// MARK: - Export Data Button
private var exportDataButton: some View {
@@ -1240,6 +1309,8 @@ struct SettingsView: View {
@AppStorage(UserDefaultsStore.Keys.firstLaunchDate.rawValue, store: GroupUserDefaults.groupDefaults) private var firstLaunchDate = Date()
@AppStorage(UserDefaultsStore.Keys.privacyLockEnabled.rawValue, store: GroupUserDefaults.groupDefaults) private var privacyLockEnabled = false
@AppStorage(UserDefaultsStore.Keys.hapticFeedbackEnabled.rawValue, store: GroupUserDefaults.groupDefaults) private var hapticFeedbackEnabled = true
@AppStorage(UserDefaultsStore.Keys.weatherEnabled.rawValue, store: GroupUserDefaults.groupDefaults) private var weatherEnabled = true
@State private var showWeatherSubscriptionStore = false
private var textColor: Color { theme.currentTheme.labelColor }
@@ -1257,6 +1328,7 @@ struct SettingsView: View {
featuresSectionHeader
privacyLockToggle
healthKitToggle
weatherToggle
exportDataButton
// Settings section
@@ -1560,6 +1632,68 @@ struct SettingsView: View {
}
}
// MARK: - Weather Toggle
private var weatherToggle: some View {
VStack(spacing: 0) {
HStack(spacing: 12) {
Image(systemName: "cloud.sun.fill")
.font(.title2)
.foregroundColor(.blue)
.frame(width: 32)
VStack(alignment: .leading, spacing: 2) {
Text("Weather")
.foregroundColor(textColor)
Text("Show weather details for each day")
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
Toggle("", isOn: Binding(
get: { iapManager.shouldShowPaywall ? false : weatherEnabled },
set: { newValue in
if iapManager.shouldShowPaywall {
showWeatherSubscriptionStore = true
return
}
weatherEnabled = newValue
if newValue {
LocationManager.shared.requestAuthorization()
}
AnalyticsManager.shared.track(.weatherToggled(enabled: newValue))
}
))
.labelsHidden()
.disabled(iapManager.shouldShowPaywall)
}
.padding()
if iapManager.shouldShowPaywall {
HStack {
Image(systemName: "crown.fill")
.foregroundColor(.yellow)
Text("Premium Feature")
.font(.caption)
.foregroundStyle(.secondary)
}
.padding(.horizontal)
.padding(.bottom, 12)
}
}
.background(theme.currentTheme.secondaryBGColor)
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
.sheet(isPresented: $showWeatherSubscriptionStore) {
ReflectSubscriptionStoreView(source: "settings")
}
}
// MARK: - Export Data Button
private var exportDataButton: some View {