Add premium features and reorganize Settings tab

Premium Features:
- Journal notes and photo attachments for mood entries
- Data export (CSV and PDF reports)
- Privacy lock with Face ID/Touch ID
- Apple Health integration for mood correlation
- 4 new personality packs (Motivational Coach, Zen Master, Best Friend, Data Analyst)

Settings Tab Reorganization:
- Combined Customize and Settings into single tab with segmented control
- Added upgrade banner with trial countdown above segment
- "Why Upgrade?" sheet showing all premium benefits
- Subscribe button opens improved StoreKit 2 subscription view

UI Improvements:
- Enhanced subscription store with feature highlights
- Entry detail view for viewing/editing notes and photos
- Removed duplicate subscription banners from tab content

🤖 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-13 12:22:06 -06:00
parent 6c92cf4ec3
commit 920aaee35c
26 changed files with 4295 additions and 99 deletions

View File

@@ -35,6 +35,10 @@ final class MoodEntryModel {
var canEdit: Bool
var canDelete: Bool
// Journal & Media (NEW)
var notes: String?
var photoID: UUID?
// Computed properties
var mood: Mood {
Mood(rawValue: moodValue) ?? .missing
@@ -49,7 +53,9 @@ final class MoodEntryModel {
mood: Mood,
entryType: EntryType,
canEdit: Bool = true,
canDelete: Bool = true
canDelete: Bool = true,
notes: String? = nil,
photoID: UUID? = nil
) {
self.forDate = forDate
self.moodValue = mood.rawValue
@@ -58,6 +64,8 @@ final class MoodEntryModel {
self.entryType = entryType.rawValue
self.canEdit = canEdit
self.canDelete = canDelete
self.notes = notes
self.photoID = photoID
}
// Convenience initializer for raw values
@@ -68,7 +76,9 @@ final class MoodEntryModel {
timestamp: Date = Date(),
weekDay: Int? = nil,
canEdit: Bool = true,
canDelete: Bool = true
canDelete: Bool = true,
notes: String? = nil,
photoID: UUID? = nil
) {
self.forDate = forDate
self.moodValue = moodValue
@@ -77,5 +87,7 @@ final class MoodEntryModel {
self.entryType = entryType
self.canEdit = canEdit
self.canDelete = canDelete
self.notes = notes
self.photoID = photoID
}
}

View File

@@ -16,12 +16,16 @@ protocol PersonalityPackable {
}
enum PersonalityPack: Int, CaseIterable {
case Default
case Rude
case Default = 0
case Rude = 1
case MotivationalCoach = 2
case ZenMaster = 3
case BestFriend = 4
case DataAnalyst = 5
func randomPushNotificationStrings() -> (title: String, body: String) {
let onboarding = UserDefaultsStore.getOnboarding()
switch (self, onboarding.inputDay) {
case (.Default, .Today):
return (DefaultTitles.notificationTitles.randomElement()!,
@@ -35,15 +39,69 @@ enum PersonalityPack: Int, CaseIterable {
case (.Rude, .Previous):
return (RudeTitles.notificationTitles.randomElement()!,
RudeTitles.notificationBodyYesterday.randomElement()!)
case (.MotivationalCoach, .Today):
return (MotivationalCoachTitles.notificationTitles.randomElement()!,
MotivationalCoachTitles.notificationBodyToday.randomElement()!)
case (.MotivationalCoach, .Previous):
return (MotivationalCoachTitles.notificationTitles.randomElement()!,
MotivationalCoachTitles.notificationBodyYesterday.randomElement()!)
case (.ZenMaster, .Today):
return (ZenMasterTitles.notificationTitles.randomElement()!,
ZenMasterTitles.notificationBodyToday.randomElement()!)
case (.ZenMaster, .Previous):
return (ZenMasterTitles.notificationTitles.randomElement()!,
ZenMasterTitles.notificationBodyYesterday.randomElement()!)
case (.BestFriend, .Today):
return (BestFriendTitles.notificationTitles.randomElement()!,
BestFriendTitles.notificationBodyToday.randomElement()!)
case (.BestFriend, .Previous):
return (BestFriendTitles.notificationTitles.randomElement()!,
BestFriendTitles.notificationBodyYesterday.randomElement()!)
case (.DataAnalyst, .Today):
return (DataAnalystTitles.notificationTitles.randomElement()!,
DataAnalystTitles.notificationBodyToday.randomElement()!)
case (.DataAnalyst, .Previous):
return (DataAnalystTitles.notificationTitles.randomElement()!,
DataAnalystTitles.notificationBodyYesterday.randomElement()!)
}
}
func title() -> String {
switch self {
case .Default:
return DefaultTitles.title
case .Rude:
return RudeTitles.title
case .MotivationalCoach:
return MotivationalCoachTitles.title
case .ZenMaster:
return ZenMasterTitles.title
case .BestFriend:
return BestFriendTitles.title
case .DataAnalyst:
return DataAnalystTitles.title
}
}
var icon: String {
switch self {
case .Default: return "face.smiling"
case .Rude: return "flame"
case .MotivationalCoach: return "figure.run"
case .ZenMaster: return "leaf"
case .BestFriend: return "heart"
case .DataAnalyst: return "chart.bar"
}
}
var description: String {
switch self {
case .Default: return "Friendly and supportive"
case .Rude: return "Snarky with attitude"
case .MotivationalCoach: return "High energy pump-up vibes"
case .ZenMaster: return "Calm and mindful"
case .BestFriend: return "Casual and supportive"
case .DataAnalyst: return "Stats-focused and objective"
}
}
}
@@ -80,7 +138,7 @@ final class DefaultTitles: PersonalityPackable {
final class RudeTitles: PersonalityPackable {
static var title = String(localized: "rude")
static var notificationTitles: [String] {
[
String(localized: "rude_notif_title_one"),
@@ -89,7 +147,7 @@ final class RudeTitles: PersonalityPackable {
String(localized: "rude_notif_title_four")
]
}
static var notificationBodyToday: [String] {
[
String(localized: "rude_notif_body_today_one"),
@@ -97,7 +155,7 @@ final class RudeTitles: PersonalityPackable {
String(localized: "rude_notif_body_today_three")
]
}
static var notificationBodyYesterday: [String] {
[
String(localized: "rude_notif_body_yesterday_one"),
@@ -106,3 +164,135 @@ final class RudeTitles: PersonalityPackable {
]
}
}
// MARK: - Motivational Coach
final class MotivationalCoachTitles: PersonalityPackable {
static var title = "Coach"
static var notificationTitles: [String] {
[
"LET'S GO!",
"Champion Check-In",
"Time to Shine!",
"You've Got This!"
]
}
static var notificationBodyToday: [String] {
[
"Every day is a chance to be AMAZING! How are you feeling today?",
"Winners track their progress! Log your mood and keep crushing it!",
"Your mental game matters! Take 10 seconds to check in with yourself.",
"Champions know their emotions! How's your energy right now?"
]
}
static var notificationBodyYesterday: [String] {
[
"Yesterday's reflection builds tomorrow's success! How did you feel?",
"Great athletes review their game tape! Log yesterday's mood!",
"No day is wasted when you learn from it! How was yesterday?",
"Every experience counts! Tell me about yesterday!"
]
}
}
// MARK: - Zen Master
final class ZenMasterTitles: PersonalityPackable {
static var title = "Zen"
static var notificationTitles: [String] {
[
"A Gentle Reminder",
"Mindful Moment",
"Inner Peace",
"Present Awareness"
]
}
static var notificationBodyToday: [String] {
[
"Breathe. Notice. How does this moment find you?",
"The river of feelings flows through us all. What flows through you now?",
"Like clouds passing, emotions come and go. What passes through you today?",
"In stillness, we find clarity. Take a moment to notice your inner weather."
]
}
static var notificationBodyYesterday: [String] {
[
"Yesterday has passed like a leaf on the stream. How did it feel?",
"Reflecting on the past with compassion... How was yesterday's journey?",
"Each day is a teacher. What did yesterday's emotions teach you?",
"With gentle awareness, recall yesterday. What arose within you?"
]
}
}
// MARK: - Best Friend
final class BestFriendTitles: PersonalityPackable {
static var title = "Bestie"
static var notificationTitles: [String] {
[
"Hey you!",
"Quick check-in!",
"Thinking of you!",
"Got a sec?"
]
}
static var notificationBodyToday: [String] {
[
"Just checking in on my favorite person! How's it going today?",
"Hey! Tell me everything - how are you feeling right now?",
"You know I always want to hear about your day! What's the vibe?",
"Sending good vibes your way! How are you doing today?"
]
}
static var notificationBodyYesterday: [String] {
[
"Wait, we didn't catch up yesterday! How did it go?",
"I realized I didn't hear about yesterday - fill me in!",
"Oops, missed you yesterday! How was your day?",
"Tell me about yesterday! I want to hear all about it!"
]
}
}
// MARK: - Data Analyst
final class DataAnalystTitles: PersonalityPackable {
static var title = "Analyst"
static var notificationTitles: [String] {
[
"Data Point Required",
"Daily Metric Input",
"Mood Tracking Alert",
"Status Update Needed"
]
}
static var notificationBodyToday: [String] {
[
"Today's emotional data point is pending. Please log your current mood state.",
"Incomplete dataset detected. Input today's mood to maintain tracking accuracy.",
"Daily mood metric collection: What is your current emotional status (1-5)?",
"Recording today's baseline. Please submit your mood data for analysis."
]
}
static var notificationBodyYesterday: [String] {
[
"Gap in historical data: Yesterday's mood entry is missing. Please backfill.",
"Data integrity alert: Yesterday's emotional metric was not captured.",
"Historical data request: Submit yesterday's mood for trend analysis.",
"Missing data point from previous day. Please log for complete dataset."
]
}
}

View File

@@ -68,6 +68,8 @@ class UserDefaultsStore {
case lastVotedDate
case votingLayoutStyle
case dayViewStyle
case privacyLockEnabled
case healthKitEnabled
case contentViewCurrentSelectedHeaderViewBackDays
case contentViewHeaderTag