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()