Remove #if DEBUG guards for TestFlight, polish weekly digest and insights UX
- Remove #if DEBUG from all debug settings, exporters, and IAP bypass so debug options are available in TestFlight builds - Weekly digest card: replace dismiss X with collapsible chevron caret - Weekly digest: generate on-demand when opening Insights tab if no cached digest exists (BGTask + notification kept as bonus path) - Fix digest intention text color (was .secondary, now uses theme textColor) - Add "Generate Weekly Digest" debug button in Settings - Add generating overlay on Insights tab with pulsing sparkles icon that stays visible until all sections finish loading (content at 0.2 opacity) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3275,6 +3275,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Apple Intelligence is analyzing your mood data..." : {
|
||||||
|
"comment" : "A description of the process of generating insights.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Apple Intelligence Required" : {
|
"Apple Intelligence Required" : {
|
||||||
"comment" : "A title for a card that informs the user that AI report generation requires Apple Intelligence to be enabled in Settings.",
|
"comment" : "A title for a card that informs the user that AI report generation requires Apple Intelligence to be enabled in Settings.",
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
@@ -4672,6 +4676,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Create AI digest now (shows in Insights tab)" : {
|
||||||
|
"comment" : "A description of the action of generating a digest.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Create random icons" : {
|
"Create random icons" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
@@ -9035,6 +9043,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Generate Weekly Digest" : {
|
||||||
|
"comment" : "A button that generates a weekly digest of your app's usage.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Generating AI summary..." : {
|
"Generating AI summary..." : {
|
||||||
"comment" : "Text displayed in the progress indicator while generating the AI-generated quick summary report.",
|
"comment" : "Text displayed in the progress indicator while generating the AI-generated quick summary report.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
@@ -9077,6 +9089,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Generating Insights" : {
|
||||||
|
"comment" : "A message displayed when the app is generating user insights.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Generating monthly summaries..." : {
|
"Generating monthly summaries..." : {
|
||||||
"comment" : "Message displayed in the progress view while generating monthly summaries for the detailed report.",
|
"comment" : "Message displayed in the progress view while generating monthly summaries for the detailed report.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
@@ -9465,6 +9481,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Great job completing your reflection. Taking time to check in with yourself is a powerful habit." : {
|
||||||
|
"comment" : "A fallback message displayed when the AI is unavailable.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Green dot = eligible to show. Tips only show once per session when eligible." : {
|
"Green dot = eligible to show. Tips only show once per session when eligible." : {
|
||||||
"comment" : "A footer label explaining that tips are only shown once per session and that the green dot indicates whether a tip is currently eligible to be shown.",
|
"comment" : "A footer label explaining that tips are only shown once per session and that the green dot indicates whether a tip is currently eligible to be shown.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
@@ -26903,6 +26923,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Weekly Digest" : {
|
||||||
|
"comment" : "A label displayed above the weekly digest.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"WeekTotalTemplate body" : {
|
"WeekTotalTemplate body" : {
|
||||||
"comment" : "The body text for the WeekTotalTemplate view.",
|
"comment" : "The body text for the WeekTotalTemplate view.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
@@ -27864,6 +27888,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Your Reflection" : {
|
||||||
|
"comment" : "A title describing the reflection.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Your Weekly Digest" : {
|
||||||
|
"comment" : "Title of a notification that appears weekly.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"version" : "1.1"
|
"version" : "1.1"
|
||||||
|
|||||||
@@ -37,13 +37,9 @@ class IAPManager: ObservableObject {
|
|||||||
|
|
||||||
/// Set to `true` to bypass all subscription checks and grant full access (for development only)
|
/// Set to `true` to bypass all subscription checks and grant full access (for development only)
|
||||||
/// Togglable at runtime in DEBUG builds via Settings > Debug > Bypass Subscription
|
/// Togglable at runtime in DEBUG builds via Settings > Debug > Bypass Subscription
|
||||||
#if DEBUG
|
|
||||||
@Published var bypassSubscription: Bool {
|
@Published var bypassSubscription: Bool {
|
||||||
didSet { UserDefaults.standard.set(bypassSubscription, forKey: "debug_bypassSubscription") }
|
didSet { UserDefaults.standard.set(bypassSubscription, forKey: "debug_bypassSubscription") }
|
||||||
}
|
}
|
||||||
#else
|
|
||||||
let bypassSubscription = false
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// MARK: - Constants
|
// MARK: - Constants
|
||||||
|
|
||||||
@@ -140,9 +136,7 @@ class IAPManager: ObservableObject {
|
|||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
#if DEBUG
|
|
||||||
self.bypassSubscription = UserDefaults.standard.bool(forKey: "debug_bypassSubscription")
|
self.bypassSubscription = UserDefaults.standard.bool(forKey: "debug_bypassSubscription")
|
||||||
#endif
|
|
||||||
restoreCachedSubscriptionState()
|
restoreCachedSubscriptionState()
|
||||||
updateListenerTask = listenForTransactions()
|
updateListenerTask = listenForTransactions()
|
||||||
|
|
||||||
@@ -365,7 +359,6 @@ class IAPManager: ObservableObject {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
/// Reset subscription state for UI testing. Called after group defaults are cleared
|
/// Reset subscription state for UI testing. Called after group defaults are cleared
|
||||||
/// so that stale cached state from previous test runs is discarded.
|
/// so that stale cached state from previous test runs is discarded.
|
||||||
func resetForTesting() {
|
func resetForTesting() {
|
||||||
@@ -382,7 +375,6 @@ class IAPManager: ObservableObject {
|
|||||||
|
|
||||||
updateTrialState()
|
updateTrialState()
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
private func updateTrialState() {
|
private func updateTrialState() {
|
||||||
let daysSinceInstall = Calendar.current.dateComponents([.day], from: firstLaunchDate, to: Date()).day ?? 0
|
let daysSinceInstall = Calendar.current.dateComponents([.day], from: firstLaunchDate, to: Date()).day ?? 0
|
||||||
|
|||||||
@@ -157,7 +157,6 @@ class LocalNotification {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
/// Sends one notification from each personality pack, staggered over 10 seconds for screenshot
|
/// Sends one notification from each personality pack, staggered over 10 seconds for screenshot
|
||||||
public class func sendAllPersonalityNotificationsForScreenshot() {
|
public class func sendAllPersonalityNotificationsForScreenshot() {
|
||||||
let _ = createNotificationCategory()
|
let _ = createNotificationCategory()
|
||||||
@@ -195,5 +194,4 @@ class LocalNotification {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ extension DataController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func populateMemory() {
|
func populateMemory() {
|
||||||
#if DEBUG
|
|
||||||
for idx in 1..<255 {
|
for idx in 1..<255 {
|
||||||
let date = Calendar.current.date(byAdding: .day, value: -idx, to: Date())!
|
let date = Calendar.current.date(byAdding: .day, value: -idx, to: Date())!
|
||||||
var moodValue = Int.random(in: 2...4)
|
var moodValue = Int.random(in: 2...4)
|
||||||
@@ -43,7 +42,6 @@ extension DataController {
|
|||||||
modelContext.insert(entry)
|
modelContext.insert(entry)
|
||||||
}
|
}
|
||||||
save()
|
save()
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates an entry that is NOT inserted into the context - used for UI placeholders
|
/// Creates an entry that is NOT inserted into the context - used for UI placeholders
|
||||||
@@ -79,7 +77,6 @@ extension DataController {
|
|||||||
saveAndRunDataListeners()
|
saveAndRunDataListeners()
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
func populate2YearsData() {
|
func populate2YearsData() {
|
||||||
clearDB()
|
clearDB()
|
||||||
|
|
||||||
@@ -100,7 +97,6 @@ extension DataController {
|
|||||||
|
|
||||||
saveAndRunDataListeners()
|
saveAndRunDataListeners()
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
private static func randomMood() -> Mood {
|
private static func randomMood() -> Mood {
|
||||||
var moodValue = Int.random(in: 3...4)
|
var moodValue = Int.random(in: 3...4)
|
||||||
|
|||||||
@@ -4,8 +4,6 @@
|
|||||||
//
|
//
|
||||||
// Exportable insights views with sample AI-generated insights for screenshots.
|
// Exportable insights views with sample AI-generated insights for screenshots.
|
||||||
//
|
//
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
// MARK: - Sample Insights Data
|
// MARK: - Sample Insights Data
|
||||||
@@ -377,4 +375,3 @@ struct ExportableInsightsContainer<Content: View>: View {
|
|||||||
.background(backgroundColor)
|
.background(backgroundColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|||||||
@@ -5,8 +5,6 @@
|
|||||||
// Exportable watch views that match the real watchOS layouts.
|
// Exportable watch views that match the real watchOS layouts.
|
||||||
// These views accept tint/icon configuration as parameters for batch export.
|
// These views accept tint/icon configuration as parameters for batch export.
|
||||||
//
|
//
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
// MARK: - Watch Export Configuration
|
// MARK: - Watch Export Configuration
|
||||||
@@ -362,4 +360,3 @@ struct ExportableComplicationContainer<Content: View>: View {
|
|||||||
.clipShape(isCircular ? AnyShape(Circle()) : AnyShape(RoundedRectangle(cornerRadius: 12, style: .continuous)))
|
.clipShape(isCircular ? AnyShape(Circle()) : AnyShape(RoundedRectangle(cornerRadius: 12, style: .continuous)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|||||||
@@ -5,8 +5,6 @@
|
|||||||
// Exportable widget views that match the real WidgetKit widgets pixel-for-pixel.
|
// Exportable widget views that match the real WidgetKit widgets pixel-for-pixel.
|
||||||
// These views accept tint/icon configuration as parameters for batch export.
|
// These views accept tint/icon configuration as parameters for batch export.
|
||||||
//
|
//
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
// MARK: - Widget Theme Configuration
|
// MARK: - Widget Theme Configuration
|
||||||
@@ -691,4 +689,3 @@ struct ExportableWidgetContainer<Content: View>: View {
|
|||||||
.clipShape(RoundedRectangle(cornerRadius: 24, style: .continuous))
|
.clipShape(RoundedRectangle(cornerRadius: 24, style: .continuous))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
// Debug utility to export insights view screenshots with sample AI data.
|
// Debug utility to export insights view screenshots with sample AI data.
|
||||||
//
|
//
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@@ -100,4 +99,3 @@ class InsightsExporter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
// Debug utility to export sharing template screenshots.
|
// Debug utility to export sharing template screenshots.
|
||||||
//
|
//
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@@ -173,4 +172,3 @@ class SharingScreenshotExporter {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
// Uses the exportable watch views from ExportableWatchViews.swift.
|
// Uses the exportable watch views from ExportableWatchViews.swift.
|
||||||
//
|
//
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@@ -247,4 +246,3 @@ class WatchExporter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
// Uses the real widget view layouts from ExportableWidgetViews.swift.
|
// Uses the real widget view layouts from ExportableWidgetViews.swift.
|
||||||
//
|
//
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@@ -389,4 +388,3 @@ class WidgetExporter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|||||||
@@ -84,6 +84,10 @@ struct InsightsView: View {
|
|||||||
if iapManager.shouldShowPaywall {
|
if iapManager.shouldShowPaywall {
|
||||||
paywallOverlay
|
paywallOverlay
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if selectedTab == .insights && isGeneratingInsights && !iapManager.shouldShowPaywall {
|
||||||
|
generatingOverlay
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showSubscriptionStore) {
|
.sheet(isPresented: $showSubscriptionStore) {
|
||||||
@@ -104,12 +108,22 @@ struct InsightsView: View {
|
|||||||
// MARK: - Insights Content
|
// MARK: - Insights Content
|
||||||
|
|
||||||
private func loadWeeklyDigest() {
|
private func loadWeeklyDigest() {
|
||||||
if #available(iOS 26, *), !iapManager.shouldShowPaywall {
|
guard #available(iOS 26, *), !iapManager.shouldShowPaywall else { return }
|
||||||
if let digest = FoundationModelsDigestService.shared.loadLatestDigest(),
|
|
||||||
digest.isFromCurrentWeek,
|
// Try cached digest first
|
||||||
!WeeklyDigest.isDismissed(for: digest) {
|
if let digest = FoundationModelsDigestService.shared.loadLatestDigest(),
|
||||||
|
digest.isFromCurrentWeek {
|
||||||
|
weeklyDigest = digest
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// No digest for this week — generate one on-demand
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let digest = try await FoundationModelsDigestService.shared.generateWeeklyDigest()
|
||||||
weeklyDigest = digest
|
weeklyDigest = digest
|
||||||
showDigest = true
|
} catch {
|
||||||
|
// Not enough data or AI unavailable — just don't show the card
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,10 +132,8 @@ struct InsightsView: View {
|
|||||||
ScrollView {
|
ScrollView {
|
||||||
VStack(spacing: 20) {
|
VStack(spacing: 20) {
|
||||||
// Weekly Digest Card
|
// Weekly Digest Card
|
||||||
if showDigest, let digest = weeklyDigest {
|
if let digest = weeklyDigest {
|
||||||
WeeklyDigestCardView(digest: digest) {
|
WeeklyDigestCardView(digest: digest)
|
||||||
showDigest = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This Month Section
|
// This Month Section
|
||||||
@@ -166,6 +178,8 @@ struct InsightsView: View {
|
|||||||
.padding(.vertical)
|
.padding(.vertical)
|
||||||
.padding(.bottom, 100)
|
.padding(.bottom, 100)
|
||||||
}
|
}
|
||||||
|
.opacity(isGeneratingInsights && !iapManager.shouldShowPaywall ? 0.2 : 1.0)
|
||||||
|
.animation(.easeInOut(duration: 0.3), value: isGeneratingInsights)
|
||||||
.refreshable {
|
.refreshable {
|
||||||
viewModel.refreshInsights()
|
viewModel.refreshInsights()
|
||||||
// Small delay to show refresh animation
|
// Small delay to show refresh animation
|
||||||
@@ -174,6 +188,50 @@ struct InsightsView: View {
|
|||||||
.disabled(iapManager.shouldShowPaywall)
|
.disabled(iapManager.shouldShowPaywall)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Generating State
|
||||||
|
|
||||||
|
private var isGeneratingInsights: Bool {
|
||||||
|
let states = [viewModel.monthLoadingState, viewModel.yearLoadingState, viewModel.allTimeLoadingState]
|
||||||
|
return states.contains(where: { $0 == .loading || $0 == .idle })
|
||||||
|
}
|
||||||
|
|
||||||
|
private var generatingOverlay: some View {
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
Image(systemName: "sparkles")
|
||||||
|
.font(.system(size: 36))
|
||||||
|
.foregroundStyle(
|
||||||
|
LinearGradient(
|
||||||
|
colors: [.purple, .blue],
|
||||||
|
startPoint: .leading,
|
||||||
|
endPoint: .trailing
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.symbolEffect(.pulse, options: .repeating)
|
||||||
|
|
||||||
|
Text(String(localized: "Generating Insights"))
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
|
Text(String(localized: "Apple Intelligence is analyzing your mood data..."))
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
}
|
||||||
|
.padding(32)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 24)
|
||||||
|
.fill(.regularMaterial)
|
||||||
|
)
|
||||||
|
.padding(.horizontal, 40)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.transition(.opacity)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Paywall Overlay
|
// MARK: - Paywall Overlay
|
||||||
|
|
||||||
private var paywallOverlay: some View {
|
private var paywallOverlay: some View {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import SwiftUI
|
|||||||
struct WeeklyDigestCardView: View {
|
struct WeeklyDigestCardView: View {
|
||||||
|
|
||||||
let digest: WeeklyDigest
|
let digest: WeeklyDigest
|
||||||
let onDismiss: () -> Void
|
|
||||||
|
|
||||||
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
@AppStorage(UserDefaultsStore.Keys.moodTint.rawValue, store: GroupUserDefaults.groupDefaults) private var moodTint: MoodTints = .Default
|
||||||
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
@AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system
|
||||||
@@ -18,82 +17,91 @@ struct WeeklyDigestCardView: View {
|
|||||||
private var textColor: Color { theme.currentTheme.labelColor }
|
private var textColor: Color { theme.currentTheme.labelColor }
|
||||||
private var accentColor: Color { moodTint.color(forMood: .good) }
|
private var accentColor: Color { moodTint.color(forMood: .good) }
|
||||||
|
|
||||||
|
@State private var isExpanded = true
|
||||||
@State private var appeared = false
|
@State private var appeared = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 16) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
// Header
|
// Header — always visible, tappable to toggle
|
||||||
HStack {
|
Button {
|
||||||
Image(systemName: digest.iconName)
|
withAnimation(.easeInOut(duration: 0.25)) {
|
||||||
.font(.title2)
|
isExpanded.toggle()
|
||||||
.foregroundStyle(accentColor)
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
Text(String(localized: "Weekly Digest"))
|
|
||||||
.font(.caption)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
.textCase(.uppercase)
|
|
||||||
|
|
||||||
Text(digest.headline)
|
|
||||||
.font(.headline)
|
|
||||||
.foregroundColor(textColor)
|
|
||||||
}
|
}
|
||||||
|
} label: {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: digest.iconName)
|
||||||
|
.font(.title2)
|
||||||
|
.foregroundStyle(accentColor)
|
||||||
|
|
||||||
Spacer()
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(String(localized: "Weekly Digest"))
|
||||||
|
.font(.caption)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.textCase(.uppercase)
|
||||||
|
|
||||||
Button {
|
Text(digest.headline)
|
||||||
WeeklyDigest.markDismissed()
|
.font(.headline)
|
||||||
withAnimation(.easeInOut(duration: 0.3)) {
|
.foregroundColor(textColor)
|
||||||
onDismiss()
|
.multilineTextAlignment(.leading)
|
||||||
}
|
}
|
||||||
} label: {
|
|
||||||
Image(systemName: "xmark.circle.fill")
|
Spacer()
|
||||||
.font(.title3)
|
|
||||||
|
Image(systemName: "chevron.up")
|
||||||
|
.font(.caption.weight(.semibold))
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.rotationEffect(.degrees(isExpanded ? 0 : 180))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.accessibilityIdentifier(AccessibilityID.WeeklyDigest.dismissButton)
|
||||||
|
|
||||||
|
// Expandable content
|
||||||
|
if isExpanded {
|
||||||
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
|
// Summary
|
||||||
|
Text(digest.summary)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
// Highlight
|
||||||
|
HStack(alignment: .top, spacing: 10) {
|
||||||
|
Image(systemName: "star.fill")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.yellow)
|
||||||
|
.padding(.top, 2)
|
||||||
|
|
||||||
|
Text(digest.highlight)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intention
|
||||||
|
HStack(alignment: .top, spacing: 10) {
|
||||||
|
Image(systemName: "arrow.right.circle.fill")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(accentColor)
|
||||||
|
.padding(.top, 2)
|
||||||
|
|
||||||
|
Text(digest.intention)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date range
|
||||||
|
Text(dateRangeString)
|
||||||
|
.font(.caption2)
|
||||||
.foregroundStyle(.tertiary)
|
.foregroundStyle(.tertiary)
|
||||||
}
|
}
|
||||||
.accessibilityLabel(String(localized: "Dismiss digest"))
|
.padding(.top, 16)
|
||||||
.accessibilityIdentifier(AccessibilityID.WeeklyDigest.dismissButton)
|
.transition(.opacity.combined(with: .move(edge: .top)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Summary
|
|
||||||
Text(digest.summary)
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(textColor)
|
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
|
|
||||||
// Highlight
|
|
||||||
HStack(alignment: .top, spacing: 10) {
|
|
||||||
Image(systemName: "star.fill")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.yellow)
|
|
||||||
.padding(.top, 2)
|
|
||||||
|
|
||||||
Text(digest.highlight)
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(textColor)
|
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Intention
|
|
||||||
HStack(alignment: .top, spacing: 10) {
|
|
||||||
Image(systemName: "arrow.right.circle.fill")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(accentColor)
|
|
||||||
.padding(.top, 2)
|
|
||||||
|
|
||||||
Text(digest.intention)
|
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Date range
|
|
||||||
Text(dateRangeString)
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundStyle(.tertiary)
|
|
||||||
}
|
}
|
||||||
.padding(20)
|
.padding(20)
|
||||||
.background(
|
.background(
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
struct PaywallPreviewSettingsView: View {
|
struct PaywallPreviewSettingsView: View {
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
@State private var selectedStyle: PaywallStyle = .celestial
|
@State private var selectedStyle: PaywallStyle = .celestial
|
||||||
@@ -928,4 +927,3 @@ struct JournalMiniPreview: View {
|
|||||||
.environmentObject(IAPManager())
|
.environmentObject(IAPManager())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ struct SettingsContentView: View {
|
|||||||
privacyButton
|
privacyButton
|
||||||
analyticsToggle
|
analyticsToggle
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
// Debug section
|
// Debug section
|
||||||
debugSectionHeader
|
debugSectionHeader
|
||||||
addTestDataButton
|
addTestDataButton
|
||||||
@@ -83,9 +82,9 @@ struct SettingsContentView: View {
|
|||||||
exportInsightsButton
|
exportInsightsButton
|
||||||
generateAndExportButton
|
generateAndExportButton
|
||||||
deleteHealthKitDataButton
|
deleteHealthKitDataButton
|
||||||
|
generateWeeklyDigestButton
|
||||||
|
|
||||||
clearDataButton
|
clearDataButton
|
||||||
#endif
|
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
.frame(height: 20)
|
.frame(height: 20)
|
||||||
@@ -207,7 +206,6 @@ struct SettingsContentView: View {
|
|||||||
|
|
||||||
// MARK: - Debug Section
|
// MARK: - Debug Section
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
private var debugSectionHeader: some View {
|
private var debugSectionHeader: some View {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Debug")
|
Text("Debug")
|
||||||
@@ -759,6 +757,63 @@ struct SettingsContentView: View {
|
|||||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@State private var isGeneratingDigest = false
|
||||||
|
@State private var digestResult: String?
|
||||||
|
|
||||||
|
private var generateWeeklyDigestButton: some View {
|
||||||
|
Button {
|
||||||
|
isGeneratingDigest = true
|
||||||
|
digestResult = nil
|
||||||
|
Task {
|
||||||
|
if #available(iOS 26, *) {
|
||||||
|
do {
|
||||||
|
let digest = try await FoundationModelsDigestService.shared.generateWeeklyDigest()
|
||||||
|
digestResult = "✓ \(digest.headline)"
|
||||||
|
} catch {
|
||||||
|
digestResult = "✗ \(error.localizedDescription)"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
digestResult = "✗ Requires iOS 26+"
|
||||||
|
}
|
||||||
|
isGeneratingDigest = false
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
if isGeneratingDigest {
|
||||||
|
ProgressView()
|
||||||
|
.frame(width: 32)
|
||||||
|
} else {
|
||||||
|
Image(systemName: "sparkles.rectangle.stack")
|
||||||
|
.font(.title2)
|
||||||
|
.foregroundColor(.purple)
|
||||||
|
.frame(width: 32)
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text("Generate Weekly Digest")
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
|
if let result = digestResult {
|
||||||
|
Text(result)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(result.contains("✓") ? .green : .red)
|
||||||
|
} else {
|
||||||
|
Text("Create AI digest now (shows in Insights tab)")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.disabled(isGeneratingDigest)
|
||||||
|
.background(theme.currentTheme.secondaryBGColor)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||||
|
}
|
||||||
|
|
||||||
private var clearDataButton: some View {
|
private var clearDataButton: some View {
|
||||||
Button {
|
Button {
|
||||||
MoodLogger.shared.deleteAllData()
|
MoodLogger.shared.deleteAllData()
|
||||||
@@ -787,7 +842,6 @@ struct SettingsContentView: View {
|
|||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
// MARK: - Privacy Lock Toggle
|
// MARK: - Privacy Lock Toggle
|
||||||
|
|
||||||
@@ -1394,7 +1448,6 @@ struct SettingsView: View {
|
|||||||
// specialThanksCell
|
// specialThanksCell
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
Group {
|
Group {
|
||||||
Divider()
|
Divider()
|
||||||
Text("Test builds only")
|
Text("Test builds only")
|
||||||
@@ -1410,7 +1463,6 @@ struct SettingsView: View {
|
|||||||
Divider()
|
Divider()
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
#endif
|
|
||||||
Text("\(Bundle.main.appName) v \(Bundle.main.versionNumber) (Build \(Bundle.main.buildNumber))")
|
Text("\(Bundle.main.appName) v \(Bundle.main.versionNumber) (Build \(Bundle.main.buildNumber))")
|
||||||
.font(.body)
|
.font(.body)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -246,7 +246,6 @@ extension View {
|
|||||||
|
|
||||||
// MARK: - Tips Preview View (Debug)
|
// MARK: - Tips Preview View (Debug)
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
struct TipsPreviewView: View {
|
struct TipsPreviewView: View {
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
@State private var selectedTipIndex: Int?
|
@State private var selectedTipIndex: Int?
|
||||||
@@ -384,4 +383,3 @@ private struct TipIndexWrapper: Identifiable {
|
|||||||
TipsPreviewView()
|
TipsPreviewView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|||||||
Reference in New Issue
Block a user