initial version
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
import CoreData
|
||||
import UIKit
|
||||
|
||||
@MainActor
|
||||
class SettingsViewModel: ObservableObject {
|
||||
// MARK: - Published Properties
|
||||
|
||||
@Published var isPremium = false
|
||||
@Published var isFamilyShared = false
|
||||
@Published var notificationsEnabled = false
|
||||
@Published var defaultNotificationTime = Date()
|
||||
@Published var analyticsEnabled = true
|
||||
@Published var currencyCode = AppSettings.getOrCreate(in: CoreDataStack.shared.viewContext).currency
|
||||
@Published var inputMode: InputMode = .simple
|
||||
|
||||
@Published var isLoading = false
|
||||
@Published var showingPaywall = false
|
||||
@Published var showingExportOptions = false
|
||||
@Published var showingImportSheet = false
|
||||
@Published var showingResetConfirmation = false
|
||||
@Published var errorMessage: String?
|
||||
@Published var successMessage: String?
|
||||
|
||||
// MARK: - Statistics
|
||||
|
||||
@Published var totalSources = 0
|
||||
@Published var totalSnapshots = 0
|
||||
@Published var totalCategories = 0
|
||||
|
||||
// MARK: - Dependencies
|
||||
|
||||
private let iapService: IAPService
|
||||
private let notificationService: NotificationService
|
||||
private let sourceRepository: InvestmentSourceRepository
|
||||
private let categoryRepository: CategoryRepository
|
||||
private let freemiumValidator: FreemiumValidator
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init(
|
||||
iapService: IAPService,
|
||||
notificationService: NotificationService? = nil,
|
||||
sourceRepository: InvestmentSourceRepository? = nil,
|
||||
categoryRepository: CategoryRepository? = nil
|
||||
) {
|
||||
self.iapService = iapService
|
||||
self.notificationService = notificationService ?? .shared
|
||||
self.sourceRepository = sourceRepository ?? InvestmentSourceRepository()
|
||||
self.categoryRepository = categoryRepository ?? CategoryRepository()
|
||||
self.freemiumValidator = FreemiumValidator(iapService: iapService)
|
||||
|
||||
setupObservers()
|
||||
loadSettings()
|
||||
}
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
private func setupObservers() {
|
||||
iapService.$isPremium
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: &$isPremium)
|
||||
|
||||
iapService.$isFamilyShared
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: &$isFamilyShared)
|
||||
|
||||
notificationService.$isAuthorized
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: &$notificationsEnabled)
|
||||
}
|
||||
|
||||
// MARK: - Data Loading
|
||||
|
||||
func loadSettings() {
|
||||
let context = CoreDataStack.shared.viewContext
|
||||
let settings = AppSettings.getOrCreate(in: context)
|
||||
|
||||
defaultNotificationTime = settings.defaultNotificationTime ?? Date()
|
||||
analyticsEnabled = settings.enableAnalytics
|
||||
currencyCode = settings.currency
|
||||
inputMode = InputMode(rawValue: settings.inputMode) ?? .simple
|
||||
|
||||
// Load statistics
|
||||
totalSources = sourceRepository.sourceCount
|
||||
totalCategories = categoryRepository.categories.count
|
||||
totalSnapshots = sourceRepository.sources.reduce(0) { $0 + $1.snapshotCount }
|
||||
|
||||
FirebaseService.shared.logScreenView(screenName: "Settings")
|
||||
}
|
||||
|
||||
// MARK: - Premium Actions
|
||||
|
||||
func upgradeToPremium() {
|
||||
showingPaywall = true
|
||||
FirebaseService.shared.logPaywallShown(trigger: "settings_upgrade")
|
||||
}
|
||||
|
||||
func restorePurchases() async {
|
||||
isLoading = true
|
||||
await iapService.restorePurchases()
|
||||
isLoading = false
|
||||
|
||||
if isPremium {
|
||||
successMessage = "Purchases restored successfully!"
|
||||
FirebaseService.shared.logRestorePurchases(success: true)
|
||||
} else {
|
||||
errorMessage = "No purchases to restore"
|
||||
FirebaseService.shared.logRestorePurchases(success: false)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Notification Settings
|
||||
|
||||
func requestNotificationPermission() async {
|
||||
let granted = await notificationService.requestAuthorization()
|
||||
if !granted {
|
||||
errorMessage = "Please enable notifications in Settings"
|
||||
}
|
||||
}
|
||||
|
||||
func updateNotificationTime(_ time: Date) {
|
||||
defaultNotificationTime = time
|
||||
|
||||
let context = CoreDataStack.shared.viewContext
|
||||
let settings = AppSettings.getOrCreate(in: context)
|
||||
settings.defaultNotificationTime = time
|
||||
CoreDataStack.shared.save()
|
||||
|
||||
// Reschedule all notifications with new time
|
||||
let sources = sourceRepository.fetchActiveSources()
|
||||
notificationService.scheduleAllReminders(for: sources)
|
||||
}
|
||||
|
||||
func openSystemSettings() {
|
||||
if let url = URL(string: UIApplication.openSettingsURLString) {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Export
|
||||
|
||||
func exportData(format: ExportService.ExportFormat) {
|
||||
guard freemiumValidator.canExport() else {
|
||||
showingPaywall = true
|
||||
FirebaseService.shared.logPaywallShown(trigger: "export")
|
||||
return
|
||||
}
|
||||
|
||||
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let viewController = windowScene.windows.first?.rootViewController else {
|
||||
return
|
||||
}
|
||||
|
||||
ExportService.shared.share(
|
||||
format: format,
|
||||
sources: sourceRepository.sources,
|
||||
categories: categoryRepository.categories,
|
||||
from: viewController
|
||||
)
|
||||
}
|
||||
|
||||
var canExport: Bool {
|
||||
freemiumValidator.canExport()
|
||||
}
|
||||
|
||||
// MARK: - Analytics
|
||||
|
||||
func toggleAnalytics(_ enabled: Bool) {
|
||||
analyticsEnabled = enabled
|
||||
|
||||
let context = CoreDataStack.shared.viewContext
|
||||
let settings = AppSettings.getOrCreate(in: context)
|
||||
settings.enableAnalytics = enabled
|
||||
CoreDataStack.shared.save()
|
||||
|
||||
// Note: In production, you'd also update Firebase Analytics consent
|
||||
}
|
||||
|
||||
// MARK: - Currency
|
||||
|
||||
func updateCurrency(_ code: String) {
|
||||
currencyCode = code
|
||||
let context = CoreDataStack.shared.viewContext
|
||||
let settings = AppSettings.getOrCreate(in: context)
|
||||
settings.currency = code
|
||||
CoreDataStack.shared.save()
|
||||
}
|
||||
|
||||
// MARK: - Input Mode
|
||||
|
||||
func updateInputMode(_ mode: InputMode) {
|
||||
inputMode = mode
|
||||
let context = CoreDataStack.shared.viewContext
|
||||
let settings = AppSettings.getOrCreate(in: context)
|
||||
settings.inputMode = mode.rawValue
|
||||
CoreDataStack.shared.save()
|
||||
}
|
||||
|
||||
// MARK: - Data Management
|
||||
|
||||
func resetAllData() {
|
||||
let context = CoreDataStack.shared.viewContext
|
||||
|
||||
// Delete all snapshots, sources, and categories
|
||||
for source in sourceRepository.sources {
|
||||
context.delete(source)
|
||||
}
|
||||
for category in categoryRepository.categories {
|
||||
context.delete(category)
|
||||
}
|
||||
let assetRequest: NSFetchRequest<Asset> = Asset.fetchRequest()
|
||||
let accountRequest: NSFetchRequest<Account> = Account.fetchRequest()
|
||||
let goalRequest: NSFetchRequest<Goal> = Goal.fetchRequest()
|
||||
if let assets = try? context.fetch(assetRequest) {
|
||||
assets.forEach { context.delete($0) }
|
||||
}
|
||||
if let accounts = try? context.fetch(accountRequest) {
|
||||
accounts.forEach { context.delete($0) }
|
||||
}
|
||||
if let goals = try? context.fetch(goalRequest) {
|
||||
goals.forEach { context.delete($0) }
|
||||
}
|
||||
|
||||
CoreDataStack.shared.save()
|
||||
|
||||
// Clear notifications
|
||||
notificationService.cancelAllReminders()
|
||||
|
||||
// Recreate default categories
|
||||
categoryRepository.createDefaultCategoriesIfNeeded()
|
||||
_ = AccountRepository().createDefaultAccountIfNeeded()
|
||||
|
||||
// Reload data
|
||||
loadSettings()
|
||||
|
||||
successMessage = "All data has been reset"
|
||||
}
|
||||
|
||||
// MARK: - Computed Properties
|
||||
|
||||
var appVersion: String {
|
||||
"\(AppConstants.appVersion) (\(AppConstants.buildNumber))"
|
||||
}
|
||||
|
||||
var premiumStatusText: String {
|
||||
if isPremium {
|
||||
return isFamilyShared ? "Premium (Family)" : "Premium"
|
||||
}
|
||||
return "Free"
|
||||
}
|
||||
|
||||
var sourceLimitText: String {
|
||||
if isPremium {
|
||||
return "Unlimited"
|
||||
}
|
||||
return "\(totalSources)/\(FreemiumLimits.maxSources)"
|
||||
}
|
||||
|
||||
var historyLimitText: String {
|
||||
if isPremium {
|
||||
return "Full history"
|
||||
}
|
||||
return "Last \(FreemiumLimits.maxHistoricalMonths) months"
|
||||
}
|
||||
|
||||
var storageUsedText: String {
|
||||
let bytes = calculateStorageUsed()
|
||||
let formatter = ByteCountFormatter()
|
||||
formatter.countStyle = .file
|
||||
return formatter.string(fromByteCount: Int64(bytes))
|
||||
}
|
||||
|
||||
private func calculateStorageUsed() -> Int {
|
||||
guard let storeURL = CoreDataStack.sharedStoreURL else { return 0 }
|
||||
|
||||
do {
|
||||
let attributes = try FileManager.default.attributesOfItem(atPath: storeURL.path)
|
||||
return attributes[.size] as? Int ?? 0
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user