## 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>
234 lines
8.2 KiB
Swift
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]
|
|
}
|
|
}
|
|
}
|
|
|