feat(gdpr): add data export (JSON) + account deletion — App Store and GDPR compliance

This commit is contained in:
saravanakumardb1 2026-02-27 22:57:56 -08:00
parent 3df8ac597b
commit 9be48f0abf

View File

@ -0,0 +1,210 @@
// 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?
}