211 lines
6.7 KiB
Swift
211 lines
6.7 KiB
Swift
// ── Data Export & Account Deletion Manager ────────────────────
|
|
// GDPR + App Store requirement: export all data, delete account
|
|
// All data is local — export as JSON, delete clears everything
|
|
|
|
import Foundation
|
|
import Combine
|
|
|
|
@MainActor
|
|
final class DataExportManager: ObservableObject {
|
|
static let shared = DataExportManager()
|
|
|
|
@Published var isExporting = false
|
|
@Published var isDeleting = false
|
|
@Published var lastExportURL: URL?
|
|
|
|
private init() {}
|
|
|
|
// MARK: - Export All Data
|
|
|
|
func exportAllData(
|
|
timers: [CMTimer],
|
|
moodCheckIns: [MoodCheckIn],
|
|
sleepData: SleepSummary?,
|
|
bedtimeRoutine: BedtimeRoutine?,
|
|
savedLocations: [SavedLocation],
|
|
locationTriggers: [LocationTrigger]
|
|
) -> URL? {
|
|
isExporting = true
|
|
defer { isExporting = false }
|
|
|
|
let export = DataExport(
|
|
exportDate: Date(),
|
|
appVersion: "1.0.0",
|
|
dataVersion: 1,
|
|
timers: timers.map { TimerExport(from: $0) },
|
|
moodCheckIns: moodCheckIns,
|
|
sleepSummary: sleepData,
|
|
bedtimeRoutine: bedtimeRoutine,
|
|
savedLocations: savedLocations,
|
|
locationTriggers: locationTriggers,
|
|
statistics: generateStatistics(timers: timers, checkIns: moodCheckIns)
|
|
)
|
|
|
|
do {
|
|
let encoder = JSONEncoder()
|
|
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
|
encoder.dateEncodingStrategy = .iso8601
|
|
let data = try encoder.encode(export)
|
|
|
|
let fileName = "chronomind-export-\(ISO8601DateFormatter().string(from: Date())).json"
|
|
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(fileName)
|
|
try data.write(to: tempURL)
|
|
|
|
lastExportURL = tempURL
|
|
return tempURL
|
|
} catch {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// MARK: - Delete All Data
|
|
|
|
func deleteAllData() {
|
|
isDeleting = true
|
|
|
|
// Clear all UserDefaults keys
|
|
let keysToRemove = [
|
|
"chronomind-timers",
|
|
"chronomind-timers-cloud",
|
|
"chronomind-cloud-sync-enabled",
|
|
"chronomind-last-sync",
|
|
"chronomind-mood-checkins",
|
|
"chronomind-mood-enabled",
|
|
"chronomind-bedtime-routine",
|
|
"chronomind-smart-alarm",
|
|
"chronomind-last-sleep",
|
|
"chronomind-saved-locations",
|
|
"chronomind-location-triggers",
|
|
"chronomind-synced-calendars",
|
|
"chronomind-calendar-sync-enabled",
|
|
"chronomind-calendar-last-sync",
|
|
"chronomind-crash-reports",
|
|
"chronomind-feedback",
|
|
"chronomind-gamification-streaks",
|
|
"chronomind-gamification-badges",
|
|
"chronomind-gamification-scores",
|
|
"cm_defaultUrgency",
|
|
"cm_defaultCascade",
|
|
"cm_hapticEnabled",
|
|
"cm_soundEnabled",
|
|
]
|
|
|
|
for key in keysToRemove {
|
|
UserDefaults.standard.removeObject(forKey: key)
|
|
}
|
|
|
|
// Clear shared App Group defaults
|
|
if let sharedDefaults = UserDefaults(suiteName: "group.com.chronomind.shared") {
|
|
for key in keysToRemove {
|
|
sharedDefaults.removeObject(forKey: key)
|
|
}
|
|
}
|
|
|
|
// Clear iCloud KV store
|
|
let kvStore = NSUbiquitousKeyValueStore.default
|
|
for key in kvStore.dictionaryRepresentation.keys {
|
|
if key.hasPrefix("chronomind") {
|
|
kvStore.removeObject(forKey: key)
|
|
}
|
|
}
|
|
kvStore.synchronize()
|
|
|
|
// Remove all pending notifications
|
|
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
|
|
UNUserNotificationCenter.current().removeAllDeliveredNotifications()
|
|
|
|
// Clear temp files
|
|
let tempDir = FileManager.default.temporaryDirectory
|
|
if let contents = try? FileManager.default.contentsOfDirectory(at: tempDir, includingPropertiesForKeys: nil) {
|
|
for url in contents where url.lastPathComponent.hasPrefix("chronomind") {
|
|
try? FileManager.default.removeItem(at: url)
|
|
}
|
|
}
|
|
|
|
isDeleting = false
|
|
}
|
|
|
|
// MARK: - Statistics
|
|
|
|
private func generateStatistics(timers: [CMTimer], checkIns: [MoodCheckIn]) -> ExportStatistics {
|
|
let completed = timers.filter { $0.state == .completed }
|
|
let dismissed = timers.filter { $0.state == .dismissed }
|
|
|
|
let avgEnergy: Double? = checkIns.isEmpty ? nil :
|
|
Double(checkIns.reduce(0) { $0 + $1.energy.numericValue }) / Double(checkIns.count)
|
|
|
|
return ExportStatistics(
|
|
totalTimersCreated: timers.count,
|
|
timersCompleted: completed.count,
|
|
timersDismissed: dismissed.count,
|
|
totalSnoozes: timers.reduce(0) { $0 + $1.snoozeCount },
|
|
totalMoodCheckIns: checkIns.count,
|
|
averageEnergyLevel: avgEnergy,
|
|
firstTimerDate: timers.map(\.createdAt).min(),
|
|
lastTimerDate: timers.map(\.createdAt).max()
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Import support
|
|
|
|
import UserNotifications
|
|
|
|
// MARK: - Export Models
|
|
|
|
struct DataExport: Codable {
|
|
let exportDate: Date
|
|
let appVersion: String
|
|
let dataVersion: Int
|
|
let timers: [TimerExport]
|
|
let moodCheckIns: [MoodCheckIn]
|
|
let sleepSummary: SleepSummary?
|
|
let bedtimeRoutine: BedtimeRoutine?
|
|
let savedLocations: [SavedLocation]
|
|
let locationTriggers: [LocationTrigger]
|
|
let statistics: ExportStatistics
|
|
}
|
|
|
|
struct TimerExport: Codable {
|
|
let id: String
|
|
let label: String
|
|
let type: String
|
|
let state: String
|
|
let urgency: String
|
|
let duration: TimeInterval
|
|
let targetTime: Date
|
|
let createdAt: Date
|
|
let completedAt: Date?
|
|
let dismissedAt: Date?
|
|
let snoozeCount: Int
|
|
let category: String?
|
|
let cascadePreset: String
|
|
|
|
init(from timer: CMTimer) {
|
|
self.id = timer.id
|
|
self.label = timer.label
|
|
self.type = timer.type.rawValue
|
|
self.state = timer.state.rawValue
|
|
self.urgency = timer.urgency.rawValue
|
|
self.duration = timer.duration
|
|
self.targetTime = timer.targetTime
|
|
self.createdAt = timer.createdAt
|
|
self.completedAt = timer.completedAt
|
|
self.dismissedAt = timer.dismissedAt
|
|
self.snoozeCount = timer.snoozeCount
|
|
self.category = timer.category
|
|
self.cascadePreset = timer.cascade.preset.rawValue
|
|
}
|
|
}
|
|
|
|
struct ExportStatistics: Codable {
|
|
let totalTimersCreated: Int
|
|
let timersCompleted: Int
|
|
let timersDismissed: Int
|
|
let totalSnoozes: Int
|
|
let totalMoodCheckIns: Int
|
|
let averageEnergyLevel: Double?
|
|
let firstTimerDate: Date?
|
|
let lastTimerDate: Date?
|
|
}
|