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:
@@ -84,6 +84,10 @@ struct InsightsView: View {
|
||||
if iapManager.shouldShowPaywall {
|
||||
paywallOverlay
|
||||
}
|
||||
|
||||
if selectedTab == .insights && isGeneratingInsights && !iapManager.shouldShowPaywall {
|
||||
generatingOverlay
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showSubscriptionStore) {
|
||||
@@ -104,12 +108,22 @@ struct InsightsView: View {
|
||||
// MARK: - Insights Content
|
||||
|
||||
private func loadWeeklyDigest() {
|
||||
if #available(iOS 26, *), !iapManager.shouldShowPaywall {
|
||||
if let digest = FoundationModelsDigestService.shared.loadLatestDigest(),
|
||||
digest.isFromCurrentWeek,
|
||||
!WeeklyDigest.isDismissed(for: digest) {
|
||||
guard #available(iOS 26, *), !iapManager.shouldShowPaywall else { return }
|
||||
|
||||
// Try cached digest first
|
||||
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
|
||||
showDigest = true
|
||||
} catch {
|
||||
// Not enough data or AI unavailable — just don't show the card
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,10 +132,8 @@ struct InsightsView: View {
|
||||
ScrollView {
|
||||
VStack(spacing: 20) {
|
||||
// Weekly Digest Card
|
||||
if showDigest, let digest = weeklyDigest {
|
||||
WeeklyDigestCardView(digest: digest) {
|
||||
showDigest = false
|
||||
}
|
||||
if let digest = weeklyDigest {
|
||||
WeeklyDigestCardView(digest: digest)
|
||||
}
|
||||
|
||||
// This Month Section
|
||||
@@ -166,6 +178,8 @@ struct InsightsView: View {
|
||||
.padding(.vertical)
|
||||
.padding(.bottom, 100)
|
||||
}
|
||||
.opacity(isGeneratingInsights && !iapManager.shouldShowPaywall ? 0.2 : 1.0)
|
||||
.animation(.easeInOut(duration: 0.3), value: isGeneratingInsights)
|
||||
.refreshable {
|
||||
viewModel.refreshInsights()
|
||||
// Small delay to show refresh animation
|
||||
@@ -174,6 +188,50 @@ struct InsightsView: View {
|
||||
.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
|
||||
|
||||
private var paywallOverlay: some View {
|
||||
|
||||
@@ -10,7 +10,6 @@ import SwiftUI
|
||||
struct WeeklyDigestCardView: View {
|
||||
|
||||
let digest: WeeklyDigest
|
||||
let onDismiss: () -> Void
|
||||
|
||||
@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
|
||||
@@ -18,82 +17,91 @@ struct WeeklyDigestCardView: View {
|
||||
private var textColor: Color { theme.currentTheme.labelColor }
|
||||
private var accentColor: Color { moodTint.color(forMood: .good) }
|
||||
|
||||
@State private var isExpanded = true
|
||||
@State private var appeared = false
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
// Header
|
||||
HStack {
|
||||
Image(systemName: digest.iconName)
|
||||
.font(.title2)
|
||||
.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)
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
// Header — always visible, tappable to toggle
|
||||
Button {
|
||||
withAnimation(.easeInOut(duration: 0.25)) {
|
||||
isExpanded.toggle()
|
||||
}
|
||||
} 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 {
|
||||
WeeklyDigest.markDismissed()
|
||||
withAnimation(.easeInOut(duration: 0.3)) {
|
||||
onDismiss()
|
||||
Text(digest.headline)
|
||||
.font(.headline)
|
||||
.foregroundColor(textColor)
|
||||
.multilineTextAlignment(.leading)
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.font(.title3)
|
||||
|
||||
Spacer()
|
||||
|
||||
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)
|
||||
}
|
||||
.accessibilityLabel(String(localized: "Dismiss digest"))
|
||||
.accessibilityIdentifier(AccessibilityID.WeeklyDigest.dismissButton)
|
||||
.padding(.top, 16)
|
||||
.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)
|
||||
.background(
|
||||
|
||||
Reference in New Issue
Block a user