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

@@ -14,7 +14,7 @@
</array> </array>
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>
<array> <array>
<string>group.com.tt.ifeel.ifeelDebug</string> <string>group.com.tt.ifeelDebug</string>
</array> </array>
</dict> </dict>
</plist> </plist>

View File

@@ -14,6 +14,7 @@ import AppIntents
struct VoteMoodIntent: AppIntent { struct VoteMoodIntent: AppIntent {
static var title: LocalizedStringResource = "Vote Mood" static var title: LocalizedStringResource = "Vote Mood"
static var description = IntentDescription("Record your mood for today") static var description = IntentDescription("Record your mood for today")
static var openAppWhenRun: Bool { false }
@Parameter(title: "Mood") @Parameter(title: "Mood")
var moodValue: Int var moodValue: Int
@@ -26,6 +27,7 @@ struct VoteMoodIntent: AppIntent {
self.moodValue = mood.rawValue self.moodValue = mood.rawValue
} }
@MainActor
func perform() async throws -> some IntentResult { func perform() async throws -> some IntentResult {
let mood = Mood(rawValue: moodValue) ?? .average let mood = Mood(rawValue: moodValue) ?? .average
let votingDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: UserDefaultsStore.getOnboarding()) let votingDate = ShowBasedOnVoteLogics.getCurrentVotingDate(onboardingData: UserDefaultsStore.getOnboarding())
@@ -48,10 +50,19 @@ struct VoteMoodIntent: AppIntent {
struct VoteWidgetProvider: TimelineProvider { struct VoteWidgetProvider: TimelineProvider {
func placeholder(in context: Context) -> VoteWidgetEntry { 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) { 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() let entry = createEntry()
completion(entry) completion(entry)
} }
@@ -210,13 +221,12 @@ struct MoodButton: View {
.foregroundColor(moodTint.color(forMood: mood)) .foregroundColor(moodTint.color(forMood: mood))
if !isCompact { if !isCompact {
Text(mood.strValue) Text(mood.widgetDisplayName)
.font(.caption2) .font(.caption2)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
} }
} }
.buttonStyle(.plain)
} }
} }
@@ -248,7 +258,7 @@ struct VotedStatsView: View {
Text("Today") Text("Today")
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
Text(mood.strValue) Text(mood.widgetDisplayName)
.font(.headline) .font(.headline)
.foregroundColor(moodTint.color(forMood: mood)) .foregroundColor(moodTint.color(forMood: mood))
} }

View File

@@ -51,7 +51,7 @@ struct TimeLineCreator {
} else { } else {
timeLineView.append(WatchTimelineView(image: moodImages.icon(forMood: .missing), timeLineView.append(WatchTimelineView(image: moodImages.icon(forMood: .missing),
graphic: moodImages.icon(forMood: .missing), graphic: moodImages.icon(forMood: .missing),
date: Date(), date: dayStart,
color: moodTint.color(forMood: .missing), color: moodTint.color(forMood: .missing),
secondaryColor: moodTint.secondary(forMood: .missing))) secondaryColor: moodTint.secondary(forMood: .missing)))
} }
@@ -60,6 +60,30 @@ struct TimeLineCreator {
timeLineView = timeLineView.sorted(by: { $0.date > $1.date }) timeLineView = timeLineView.sorted(by: { $0.date > $1.date })
return timeLineView 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 { struct Provider: IntentTimelineProvider {
@@ -67,18 +91,25 @@ struct Provider: IntentTimelineProvider {
/* /*
placeholder for widget, no data placeholder for widget, no data
gets redacted auto gets redacted auto - uses sample data for widget picker preview
*/ */
func placeholder(in context: Context) -> SimpleEntry { func placeholder(in context: Context) -> SimpleEntry {
return SimpleEntry(date: Date(), return SimpleEntry(date: Date(),
configuration: ConfigurationIntent(), 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) -> ()) { 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(), let entry = SimpleEntry(date: Date(),
configuration: ConfigurationIntent(), configuration: ConfigurationIntent(),
timeLineViews: Array(TimeLineCreator.createViews(daysBack: 11).prefix(10))) timeLineViews: timeLineViews)
completion(entry) completion(entry)
} }
@@ -147,7 +178,13 @@ struct SmallWidgetView: View {
init(entry: Provider.Entry) { init(entry: Provider.Entry) {
self.entry = 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 { var body: some View {
@@ -172,7 +209,13 @@ struct MediumWidgetView: View {
init(entry: Provider.Entry) { init(entry: Provider.Entry) {
self.entry = 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 { var body: some View {
@@ -199,7 +242,13 @@ struct LargeWidgetView: View {
init(entry: Provider.Entry) { init(entry: Provider.Entry) {
self.entry = 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) { var firstGroup: ([WatchTimelineView], String) {
@@ -258,7 +307,13 @@ struct SmallGraphicWidgetView: View {
init(entry: Provider.Entry) { init(entry: Provider.Entry) {
self.entry = 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 { var body: some View {

View File

@@ -15,7 +15,6 @@
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>
<array> <array>
<string>group.com.tt.ifeelDebug</string> <string>group.com.tt.ifeelDebug</string>
<string></string>
</array> </array>
</dict> </dict>
</plist> </plist>

View File

@@ -24,6 +24,11 @@ enum SubscriptionState: Equatable {
@MainActor @MainActor
class IAPManager: ObservableObject { 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 // MARK: - Constants
static let subscriptionGroupID = "2CFE4C4F" static let subscriptionGroupID = "2CFE4C4F"
@@ -59,6 +64,7 @@ class IAPManager: ObservableObject {
} }
var hasFullAccess: Bool { var hasFullAccess: Bool {
if Self.bypassSubscription { return true }
switch state { switch state {
case .subscribed, .inTrial: case .subscribed, .inTrial:
return true return true
@@ -68,6 +74,7 @@ class IAPManager: ObservableObject {
} }
var shouldShowPaywall: Bool { var shouldShowPaywall: Bool {
if Self.bypassSubscription { return false }
switch state { switch state {
case .trialExpired, .expired: case .trialExpired, .expired:
return true return true
@@ -134,7 +141,8 @@ class IAPManager: ObservableObject {
/// Sync subscription status to UserDefaults for widget access /// Sync subscription status to UserDefaults for widget access
private func syncSubscriptionStatusToUserDefaults() { 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 /// Restore purchases

View File

@@ -45,6 +45,19 @@ enum Mood: Int {
} }
} }
/// 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 { var color: Color {
let moodTint: MoodTintable.Type = UserDefaultsStore.moodTintable() let moodTint: MoodTintable.Type = UserDefaultsStore.moodTintable()
return moodTint.color(forMood: self) return moodTint.color(forMood: self)