From 9be48f0abf5f34d85ff5cef307d04482684ba5a1 Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Fri, 27 Feb 2026 22:57:56 -0800 Subject: [PATCH] =?UTF-8?q?feat(gdpr):=20add=20data=20export=20(JSON)=20+?= =?UTF-8?q?=20account=20deletion=20=E2=80=94=20App=20Store=20and=20GDPR=20?= =?UTF-8?q?compliance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Shared/Data/DataExportManager.swift | 210 ++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 ios/ChronoMind/Shared/Data/DataExportManager.swift diff --git a/ios/ChronoMind/Shared/Data/DataExportManager.swift b/ios/ChronoMind/Shared/Data/DataExportManager.swift new file mode 100644 index 0000000..8cb7e8a --- /dev/null +++ b/ios/ChronoMind/Shared/Data/DataExportManager.swift @@ -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? +}