Fix widget issues and add subscription bypass toggle

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 <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-12-10 10:38:16 -06:00
parent 84c0e191b1
commit 443f4dfc55
6 changed files with 123 additions and 38 deletions

View File

@@ -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..<count {
let date = Calendar.current.date(byAdding: .day, value: -i, to: Date())!
let dayStart = Calendar.current.startOfDay(for: date)
let mood = sampleMoods[i % sampleMoods.count]
timeLineView.append(WatchTimelineView(
image: moodImages.icon(forMood: mood),
graphic: moodImages.icon(forMood: mood),
date: dayStart,
color: moodTint.color(forMood: mood),
secondaryColor: moodTint.secondary(forMood: mood)
))
}
return timeLineView
}
}
struct Provider: IntentTimelineProvider {
@@ -67,18 +91,25 @@ struct Provider: IntentTimelineProvider {
/*
placeholder for widget, no data
gets redacted auto
gets redacted auto - uses sample data for widget picker preview
*/
func placeholder(in context: Context) -> 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,