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:
@@ -14,7 +14,7 @@
|
||||
</array>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.tt.ifeel.ifeelDebug</string>
|
||||
<string>group.com.tt.ifeelDebug</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.tt.ifeelDebug</string>
|
||||
<string></string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user