Files
Reflect/Shared/Models/UserDefaultsStore.swift
Trey t f2c510de50 Refactor StoreKit 2 subscription system and add interactive vote widget
## StoreKit 2 Refactor
- Rewrote IAPManager with clean enum-based state model (SubscriptionState)
- Added native SubscriptionStoreView for iOS 17+ purchase UI
- Subscription status now checked on every app launch
- Synced subscription status to UserDefaults for widget access
- Simplified PurchaseButtonView and IAPWarningView
- Removed unused StatusInfoView

## Interactive Vote Widget
- New FeelsVoteWidget with App Intents for mood voting
- Subscribers can vote directly from widget, shows stats after voting
- Non-subscribers see "Tap to subscribe" which opens subscription store
- Added feels:// URL scheme for deep linking

## Firebase Removal
- Commented out Firebase imports and initialization
- EventLogger now prints to console in DEBUG mode only

## Other Changes
- Added fallback for Core Data when App Group unavailable
- Added new localization strings for subscription UI
- Updated entitlements and Info.plist

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-09 23:07:16 -06:00

234 lines
8.2 KiB
Swift

//
// UserDefaultsStore.swift
// Feels (iOS)
//
// Created by Trey Tartt on 1/22/22.
//
import Foundation
class UserDefaultsStore {
enum Keys: String {
case savedOnboardingData
case needsOnboarding
case useCloudKit
case deleteEnable
case mainViewTopHeaderIndex
case theme
case moodImages
case moodTint
case personalityPack
case customWidget
case customMoodTint
case customMoodTintUpdateNumber
case textColor
case showNSFW
case shape
case daysFilter
case firstLaunchDate
case hasActiveSubscription
case lastVotedDate
case contentViewCurrentSelectedHeaderViewBackDays
case contentViewHeaderTag
case contentViewHeaderTagViewOneViewType
case contentViewHeaderTagViewTwoViewType
case currentSelectedHeaderViewViewType
}
static func getOnboarding() -> OnboardingData {
if let data = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.savedOnboardingData.rawValue) as? Data,
let model = try? JSONDecoder().decode(OnboardingData.self, from: data) {
return model
} else {
return OnboardingData()
}
}
static func saveOnboarding(onboardingData: OnboardingData) -> OnboardingData {
do {
let data = try JSONEncoder().encode(onboardingData)
GroupUserDefaults.groupDefaults.set(data, forKey: UserDefaultsStore.Keys.savedOnboardingData.rawValue)
return UserDefaultsStore.getOnboarding()
} catch {
fatalError("error saving")
}
}
static func moodMoodImagable() -> MoodImagable.Type {
if let data = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.moodImages.rawValue) as? Int,
let model = MoodImages.init(rawValue: data) {
return model.moodImages
} else {
return MoodImages.FontAwesome.moodImages
}
}
static func moodTintable() -> MoodTintable.Type {
if let data = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.moodTint.rawValue) as? Int,
let model = MoodTints.init(rawValue: data) {
return model.moodTints
} else {
return MoodTints.Default.moodTints
}
}
static func personalityPackable() -> PersonalityPack {
if let data = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.personalityPack.rawValue) as? Int,
let model = PersonalityPack.init(rawValue: data) {
return model
} else {
return PersonalityPack.Default
}
}
static func theme() -> Theme {
if let data = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.theme.rawValue) as? Int,
let model = Theme.init(rawValue: data) {
return model
} else {
return Theme.system
}
}
static func getCustomWidgets() -> [CustomWidgetModel] {
if let data = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.customWidget.rawValue) as? Data,
let model = try? JSONDecoder().decode([CustomWidgetModel].self, from: data) {
return model
} else {
GroupUserDefaults.groupDefaults.removeObject(forKey: UserDefaultsStore.Keys.customWidget.rawValue)
let widget = CustomWidgetModel.randomWidget
widget.isSaved = true
let widgets = [widget]
let data = try! JSONEncoder().encode(widgets)
GroupUserDefaults.groupDefaults.set(data, forKey: UserDefaultsStore.Keys.customWidget.rawValue)
if let data = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.customWidget.rawValue) as? Data,
let models = try? JSONDecoder().decode([CustomWidgetModel].self, from: data) {
let sorted = models.sorted(by: {
$0.createdDate < $1.createdDate
})
return sorted
} else {
fatalError("error getting widgets")
}
}
}
@discardableResult
static func saveCustomWidget(widgetModel: CustomWidgetModel, inUse: Bool) -> [CustomWidgetModel] {
do {
var existingWidgets = getCustomWidgets()
if let exisitingWidget = existingWidgets.firstIndex(where: {
$0.uuid == widgetModel.uuid
}) {
existingWidgets.remove(at: exisitingWidget)
// give it differnet uuid so the view updates
widgetModel.uuid = UUID().uuidString
}
if inUse {
existingWidgets.forEach({
$0.inUse = false
})
widgetModel.inUse = true
}
existingWidgets.append(widgetModel)
existingWidgets.forEach({
$0.isSaved = true
})
let data = try JSONEncoder().encode(existingWidgets)
GroupUserDefaults.groupDefaults.set(data, forKey: UserDefaultsStore.Keys.customWidget.rawValue)
return UserDefaultsStore.getCustomWidgets()
} catch {
fatalError("error saving")
}
}
@discardableResult
static func deleteCustomWidget(withUUID uuid: String) -> [CustomWidgetModel] {
do {
var existingWidgets = getCustomWidgets()
if let exisitingWidget = existingWidgets.firstIndex(where: {
$0.uuid == uuid
}) {
existingWidgets.remove(at: exisitingWidget)
}
if existingWidgets.count == 0 {
let widget = CustomWidgetModel.randomWidget
widget.isSaved = true
widget.inUse = true
existingWidgets.append(widget)
}
if let _ = existingWidgets.first(where: {
$0.inUse == true
}) {} else {
if let first = existingWidgets.first {
first.inUse = true
}
}
let data = try JSONEncoder().encode(existingWidgets)
GroupUserDefaults.groupDefaults.set(data, forKey: UserDefaultsStore.Keys.customWidget.rawValue)
return UserDefaultsStore.getCustomWidgets()
} catch {
fatalError("error saving")
}
}
static func getCustomMoodTint() -> SavedMoodTint {
if let data = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.customMoodTint.rawValue) as? Data{
do {
let model = try JSONDecoder().decode(SavedMoodTint.self, from: data)
return model
} catch {
print(error)
}
}
return SavedMoodTint()
}
static func getCustomBGShape() -> BGShape {
if let data = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.shape.rawValue) as? Int,
let model = BGShape.init(rawValue: data) {
return model
} else {
return BGShape.circle
}
}
@discardableResult
static func saveCustomMoodTint(customTint: SavedMoodTint) -> SavedMoodTint {
do {
let data = try JSONEncoder().encode(customTint)
GroupUserDefaults.groupDefaults.set(data, forKey: UserDefaultsStore.Keys.customMoodTint.rawValue)
return UserDefaultsStore.getCustomMoodTint()
} catch {
print(error)
fatalError("error saving")
}
}
@discardableResult
static func saveDaysFilter(days: [Int]) -> [Int] {
GroupUserDefaults.groupDefaults.set(days, forKey: UserDefaultsStore.Keys.daysFilter.rawValue)
return UserDefaultsStore.getDaysFilter()
}
static func getDaysFilter() -> [Int] {
if let data = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.daysFilter.rawValue) as? [Int] {
return data
} else {
return [1,2,3,4,5,6,7]
}
}
}