From 443f4dfc55f2e2451c9788d89a5f859a05d566b6 Mon Sep 17 00:00:00 2001 From: Trey t Date: Wed, 10 Dec 2025 10:38:16 -0600 Subject: [PATCH] Fix widget issues and add subscription bypass toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Widget fixes: - Fix App Group ID mismatch in iOS app entitlements (was group.com.tt.ifeel.ifeelDebug, now group.com.tt.ifeelDebug) - Fix date bug where missing entries all showed same date - Add sample data preview for widget picker (shows realistic mood data) - Add widgetDisplayName to Mood enum for widget localization - Update Mood Vote widget preview to show post-vote state - Attempt to fix interactive widget buttons (openAppWhenRun: false) Developer improvements: - Add IAPManager.bypassSubscription toggle for testing without subscription 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- Feels (iOS)Dev.entitlements | 2 +- FeelsWidget2/FeelsVoteWidget.swift | 18 ++++- FeelsWidget2/FeelsWidget.swift | 117 ++++++++++++++++++++------- FeelsWidgetExtensionDev.entitlements | 1 - Shared/IAPManager.swift | 10 ++- Shared/Models/Mood.swift | 13 +++ 6 files changed, 123 insertions(+), 38 deletions(-) diff --git a/Feels (iOS)Dev.entitlements b/Feels (iOS)Dev.entitlements index 9a8746e..cd8c8e3 100644 --- a/Feels (iOS)Dev.entitlements +++ b/Feels (iOS)Dev.entitlements @@ -14,7 +14,7 @@ com.apple.security.application-groups - group.com.tt.ifeel.ifeelDebug + group.com.tt.ifeelDebug diff --git a/FeelsWidget2/FeelsVoteWidget.swift b/FeelsWidget2/FeelsVoteWidget.swift index e3cd6a1..d183cc8 100644 --- a/FeelsWidget2/FeelsVoteWidget.swift +++ b/FeelsWidget2/FeelsVoteWidget.swift @@ -14,6 +14,7 @@ import AppIntents struct VoteMoodIntent: AppIntent { static var title: LocalizedStringResource = "Vote Mood" static var description = IntentDescription("Record your mood for today") + static var openAppWhenRun: Bool { false } @Parameter(title: "Mood") var moodValue: Int @@ -26,6 +27,7 @@ struct VoteMoodIntent: AppIntent { self.moodValue = mood.rawValue } + @MainActor func perform() async throws -> some IntentResult { let mood = Mood(rawValue: moodValue) ?? .average let votingDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: UserDefaultsStore.getOnboarding()) @@ -48,10 +50,19 @@ struct VoteMoodIntent: AppIntent { struct VoteWidgetProvider: TimelineProvider { func placeholder(in context: Context) -> VoteWidgetEntry { - VoteWidgetEntry(date: Date(), hasSubscription: true, hasVotedToday: false, todaysMood: nil, stats: nil) + // Show sample "already voted" state for widget picker preview + let sampleStats = MoodStats(totalEntries: 30, moodCounts: [.great: 10, .good: 12, .average: 5, .bad: 2, .horrible: 1]) + return VoteWidgetEntry(date: Date(), hasSubscription: true, hasVotedToday: true, todaysMood: .good, stats: sampleStats) } func getSnapshot(in context: Context, completion: @escaping (VoteWidgetEntry) -> Void) { + // Show sample data for widget picker preview + if context.isPreview { + let sampleStats = MoodStats(totalEntries: 30, moodCounts: [.great: 10, .good: 12, .average: 5, .bad: 2, .horrible: 1]) + let entry = VoteWidgetEntry(date: Date(), hasSubscription: true, hasVotedToday: true, todaysMood: .good, stats: sampleStats) + completion(entry) + return + } let entry = createEntry() completion(entry) } @@ -210,13 +221,12 @@ struct MoodButton: View { .foregroundColor(moodTint.color(forMood: mood)) if !isCompact { - Text(mood.strValue) + Text(mood.widgetDisplayName) .font(.caption2) .foregroundStyle(.secondary) } } } - .buttonStyle(.plain) } } @@ -248,7 +258,7 @@ struct VotedStatsView: View { Text("Today") .font(.caption) .foregroundStyle(.secondary) - Text(mood.strValue) + Text(mood.widgetDisplayName) .font(.headline) .foregroundColor(moodTint.color(forMood: mood)) } diff --git a/FeelsWidget2/FeelsWidget.swift b/FeelsWidget2/FeelsWidget.swift index af4365b..302118d 100644 --- a/FeelsWidget2/FeelsWidget.swift +++ b/FeelsWidget2/FeelsWidget.swift @@ -30,18 +30,18 @@ class WatchTimelineView: Identifiable { struct TimeLineCreator { static func createViews(daysBack: Int) -> [WatchTimelineView] { var timeLineView = [WatchTimelineView]() - + let latestDayToShow = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: UserDefaultsStore.getOnboarding()) let dates = Array(0...daysBack).map({ Calendar.current.date(byAdding: .day, value: -$0, to: latestDayToShow)! }) - + for date in dates { let dayStart = Calendar.current.startOfDay(for: date) let dayEnd = Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: dayStart)! let moodTint: MoodTintable.Type = UserDefaultsStore.moodTintable() let moodImages: MoodImagable.Type = UserDefaultsStore.moodMoodImagable() - + if let todayEntry = PersistenceController.shared.getData(startDate: dayStart, endDate: dayEnd, includedDays: []).first { timeLineView.append(WatchTimelineView(image: moodImages.icon(forMood: todayEntry.mood), graphic: moodImages.icon(forMood: todayEntry.mood), @@ -51,15 +51,39 @@ struct TimeLineCreator { } else { timeLineView.append(WatchTimelineView(image: moodImages.icon(forMood: .missing), graphic: moodImages.icon(forMood: .missing), - date: Date(), + date: dayStart, color: moodTint.color(forMood: .missing), secondaryColor: moodTint.secondary(forMood: .missing))) } } - + timeLineView = timeLineView.sorted(by: { $0.date > $1.date }) return timeLineView } + + /// Creates sample preview data for widget picker - shows what widget looks like with mood data + static func createSampleViews(count: Int) -> [WatchTimelineView] { + var timeLineView = [WatchTimelineView]() + let sampleMoods: [Mood] = [.great, .good, .average, .good, .great, .average, .bad, .good, .great, .good, .average] + let moodTint: MoodTintable.Type = UserDefaultsStore.moodTintable() + let moodImages: MoodImagable.Type = UserDefaultsStore.moodMoodImagable() + + for i in 0.. SimpleEntry { return SimpleEntry(date: Date(), configuration: ConfigurationIntent(), - timeLineViews: Array(TimeLineCreator.createViews(daysBack: 11).prefix(10))) + timeLineViews: TimeLineCreator.createSampleViews(count: 10)) } - + func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) { + // Use sample data for widget picker preview, real data otherwise + let timeLineViews: [WatchTimelineView] + if context.isPreview { + timeLineViews = TimeLineCreator.createSampleViews(count: 10) + } else { + timeLineViews = Array(TimeLineCreator.createViews(daysBack: 11).prefix(10)) + } let entry = SimpleEntry(date: Date(), configuration: ConfigurationIntent(), - timeLineViews: Array(TimeLineCreator.createViews(daysBack: 11).prefix(10))) + timeLineViews: timeLineViews) completion(entry) } @@ -144,12 +175,18 @@ struct FeelsWidgetEntryView : View { struct SmallWidgetView: View { var entry: Provider.Entry var timeLineView = [WatchTimelineView]() - + init(entry: Provider.Entry) { self.entry = entry - timeLineView = [TimeLineCreator.createViews(daysBack: 2).first!] + let realData = TimeLineCreator.createViews(daysBack: 2) + // Check if we have any real mood data (not all missing) + let hasRealData = realData.contains { view in + let moodTint: MoodTintable.Type = UserDefaultsStore.moodTintable() + return view.color != moodTint.color(forMood: .missing) + } + timeLineView = hasRealData ? [realData.first!] : [TimeLineCreator.createSampleViews(count: 1).first!] } - + var body: some View { ZStack { Color(UIColor.secondarySystemBackground) @@ -169,25 +206,31 @@ struct SmallWidgetView: View { struct MediumWidgetView: View { var entry: Provider.Entry var timeLineView = [WatchTimelineView]() - + init(entry: Provider.Entry) { self.entry = entry - timeLineView = Array(TimeLineCreator.createViews(daysBack: 6).prefix(5)) + let realData = Array(TimeLineCreator.createViews(daysBack: 6).prefix(5)) + // Check if we have any real mood data (not all missing) + let hasRealData = realData.contains { view in + let moodTint: MoodTintable.Type = UserDefaultsStore.moodTintable() + return view.color != moodTint.color(forMood: .missing) + } + timeLineView = hasRealData ? realData : TimeLineCreator.createSampleViews(count: 5) } - + var body: some View { VStack { Spacer() - + TimeHeaderView(startDate: timeLineView.first!.date, endDate: timeLineView.last!.date) .frame(minWidth: 0, maxWidth: .infinity) .multilineTextAlignment(.leading) - + TimeBodyView(group: timeLineView) .clipShape(RoundedRectangle(cornerRadius: 25, style: .continuous)) .frame(minHeight: 0, maxHeight: 55) .padding() - + Spacer() } } @@ -196,41 +239,47 @@ struct MediumWidgetView: View { struct LargeWidgetView: View { var entry: Provider.Entry var timeLineView = [WatchTimelineView]() - + init(entry: Provider.Entry) { self.entry = entry - timeLineView = Array(TimeLineCreator.createViews(daysBack: 11).prefix(10)) + let realData = Array(TimeLineCreator.createViews(daysBack: 11).prefix(10)) + // Check if we have any real mood data (not all missing) + let hasRealData = realData.contains { view in + let moodTint: MoodTintable.Type = UserDefaultsStore.moodTintable() + return view.color != moodTint.color(forMood: .missing) + } + timeLineView = hasRealData ? realData : TimeLineCreator.createSampleViews(count: 10) } - + var firstGroup: ([WatchTimelineView], String) { return (Array(self.timeLineView.prefix(5)), UUID().uuidString) } - + var secondGroup: ([WatchTimelineView], String) { return (Array(self.timeLineView.suffix(5)), UUID().uuidString) } - + var body: some View { VStack { Spacer() - + ForEach([firstGroup, secondGroup], id: \.1) { group in VStack { Spacer() - + TimeHeaderView(startDate: group.0.first!.date, endDate: group.0.last!.date) .frame(minWidth: 0, maxWidth: .infinity) .multilineTextAlignment(.leading) - + TimeBodyView(group: group.0) .clipShape(RoundedRectangle(cornerRadius: 25, style: .continuous)) .frame(minHeight: 0, maxHeight: 55) .padding() - + Spacer() } } - + Spacer() } } @@ -255,12 +304,18 @@ struct FeelsGraphicWidgetEntryView : View { struct SmallGraphicWidgetView: View { var entry: Provider.Entry var timeLineView: [WatchTimelineView] - + init(entry: Provider.Entry) { self.entry = entry - timeLineView = TimeLineCreator.createViews(daysBack: 2) + let realData = TimeLineCreator.createViews(daysBack: 2) + // Check if we have any real mood data (not all missing) + let hasRealData = realData.contains { view in + let moodTint: MoodTintable.Type = UserDefaultsStore.moodTintable() + return view.color != moodTint.color(forMood: .missing) + } + timeLineView = hasRealData ? realData : TimeLineCreator.createSampleViews(count: 2) } - + var body: some View { if let first = timeLineView.first { IconView(iconViewModel: IconViewModel(backgroundImage: first.graphic, diff --git a/FeelsWidgetExtensionDev.entitlements b/FeelsWidgetExtensionDev.entitlements index 8c54224..cd8c8e3 100644 --- a/FeelsWidgetExtensionDev.entitlements +++ b/FeelsWidgetExtensionDev.entitlements @@ -15,7 +15,6 @@ com.apple.security.application-groups group.com.tt.ifeelDebug - diff --git a/Shared/IAPManager.swift b/Shared/IAPManager.swift index 61eba37..95900fc 100644 --- a/Shared/IAPManager.swift +++ b/Shared/IAPManager.swift @@ -24,6 +24,11 @@ enum SubscriptionState: Equatable { @MainActor class IAPManager: ObservableObject { + // MARK: - Debug Toggle + + /// Set to `true` to bypass all subscription checks and grant full access (for development only) + static let bypassSubscription = true + // MARK: - Constants static let subscriptionGroupID = "2CFE4C4F" @@ -59,6 +64,7 @@ class IAPManager: ObservableObject { } var hasFullAccess: Bool { + if Self.bypassSubscription { return true } switch state { case .subscribed, .inTrial: return true @@ -68,6 +74,7 @@ class IAPManager: ObservableObject { } var shouldShowPaywall: Bool { + if Self.bypassSubscription { return false } switch state { case .trialExpired, .expired: return true @@ -134,7 +141,8 @@ class IAPManager: ObservableObject { /// Sync subscription status to UserDefaults for widget access private func syncSubscriptionStatusToUserDefaults() { - GroupUserDefaults.groupDefaults.set(hasFullAccess, forKey: UserDefaultsStore.Keys.hasActiveSubscription.rawValue) + let accessValue = Self.bypassSubscription ? true : hasFullAccess + GroupUserDefaults.groupDefaults.set(accessValue, forKey: UserDefaultsStore.Keys.hasActiveSubscription.rawValue) } /// Restore purchases diff --git a/Shared/Models/Mood.swift b/Shared/Models/Mood.swift index 470195d..bf34c2a 100644 --- a/Shared/Models/Mood.swift +++ b/Shared/Models/Mood.swift @@ -44,6 +44,19 @@ enum Mood: Int { return String("placeholder") } } + + /// Non-localized display name for use in widgets (which don't have access to app's localization) + var widgetDisplayName: String { + switch self { + case .horrible: return "Horrible" + case .bad: return "Bad" + case .average: return "Average" + case .good: return "Good" + case .great: return "Great" + case .missing: return "Missing" + case .placeholder: return "Placeholder" + } + } var color: Color { let moodTint: MoodTintable.Type = UserDefaultsStore.moodTintable()