diff --git a/ios/Plugin/DailyNotificationPlugin.swift b/ios/Plugin/DailyNotificationPlugin.swift index 7a90bad..cebed69 100644 --- a/ios/Plugin/DailyNotificationPlugin.swift +++ b/ios/Plugin/DailyNotificationPlugin.swift @@ -947,6 +947,276 @@ public class DailyNotificationPlugin: CAPPlugin { return (now.addingTimeInterval(24 * 60 * 60).timeIntervalSince1970 * 1000) } + // MARK: - History Methods + + /** + * Get history + * + * Returns history entries with optional filters (since, kind, limit). + * + * Equivalent to Android's getHistory method. + */ + @objc func getHistory(_ call: CAPPluginCall) { + let options = call.getObject("options") + let since = options?["since"] as? Int64 + let kind = options?["kind"] as? String + let limit = (options?["limit"] as? Int) ?? 50 + + print("DNP-PLUGIN: Getting history: since=\(since?.description ?? "none"), kind=\(kind ?? "all"), limit=\(limit)") + + Task { + do { + let context = persistenceController.container.viewContext + let request: NSFetchRequest = History.fetchRequest() + + // Build predicate + var predicates: [NSPredicate] = [] + + if let sinceTimestamp = since { + let sinceDate = Date(timeIntervalSince1970: TimeInterval(sinceTimestamp) / 1000.0) + predicates.append(NSPredicate(format: "occurredAt >= %@", sinceDate as NSDate)) + } + + if let kindFilter = kind { + predicates.append(NSPredicate(format: "kind == %@", kindFilter)) + } + + if !predicates.isEmpty { + request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates) + } + + // Sort by occurredAt descending (most recent first) + request.sortDescriptors = [NSSortDescriptor(keyPath: \History.occurredAt, ascending: false)] + request.fetchLimit = limit + + let results = try context.fetch(request) + + let historyArray = results.map { history -> [String: Any] in + [ + "id": history.id ?? "", + "refId": history.refId ?? "", + "kind": history.kind ?? "", + "occurredAt": Int64((history.occurredAt?.timeIntervalSince1970 ?? 0) * 1000), + "durationMs": history.durationMs, + "outcome": history.outcome ?? "", + "diagJson": history.diagJson ?? "" + ] + } + + let result: [String: Any] = [ + "history": historyArray + ] + + print("DNP-PLUGIN: Found \(historyArray.count) history entry(ies)") + + DispatchQueue.main.async { + call.resolve(result) + } + } catch { + print("DNP-PLUGIN: Failed to get history: \(error)") + DispatchQueue.main.async { + call.reject("Failed to get history: \(error.localizedDescription)") + } + } + } + } + + /** + * Get history stats + * + * Returns statistics about history entries. + * + * Equivalent to Android's getHistoryStats method. + */ + @objc func getHistoryStats(_ call: CAPPluginCall) { + print("DNP-PLUGIN: Getting history stats") + + Task { + do { + let context = persistenceController.container.viewContext + let request: NSFetchRequest = History.fetchRequest() + request.sortDescriptors = [NSSortDescriptor(keyPath: \History.occurredAt, ascending: false)] + + let allHistory = try context.fetch(request) + + var outcomes: [String: Int] = [:] + var kinds: [String: Int] = [:] + var mostRecent: TimeInterval? = nil + var oldest: TimeInterval? = nil + + for entry in allHistory { + // Count outcomes + if let outcome = entry.outcome { + outcomes[outcome] = (outcomes[outcome] ?? 0) + 1 + } + + // Count kinds + if let kind = entry.kind { + kinds[kind] = (kinds[kind] ?? 0) + 1 + } + + // Track timestamps + if let occurredAt = entry.occurredAt { + let timestamp = occurredAt.timeIntervalSince1970 * 1000 + if mostRecent == nil || timestamp > mostRecent! { + mostRecent = timestamp + } + if oldest == nil || timestamp < oldest! { + oldest = timestamp + } + } + } + + let result: [String: Any] = [ + "totalCount": allHistory.count, + "outcomes": outcomes, + "kinds": kinds, + "mostRecent": mostRecent ?? NSNull(), + "oldest": oldest ?? NSNull() + ] + + print("DNP-PLUGIN: History stats: total=\(allHistory.count), outcomes=\(outcomes.count), kinds=\(kinds.count)") + + DispatchQueue.main.async { + call.resolve(result) + } + } catch { + print("DNP-PLUGIN: Failed to get history stats: \(error)") + DispatchQueue.main.async { + call.reject("Failed to get history stats: \(error.localizedDescription)") + } + } + } + } + + // MARK: - Config Methods + + /** + * Get all configs + * + * Returns all configurations matching optional filters. + * + * Equivalent to Android's getAllConfigs method. + */ + @objc func getAllConfigs(_ call: CAPPluginCall) { + let options = call.getObject("options") + let timesafariDid = options?["timesafariDid"] as? String + let configType = options?["configType"] as? String + + print("DNP-PLUGIN: Getting all configs: did=\(timesafariDid ?? "none"), type=\(configType ?? "all")") + + // Get all UserDefaults keys that start with our prefix + let prefix = "DailyNotificationConfig_" + var configs: [[String: Any]] = [] + + // Note: UserDefaults doesn't support listing all keys directly + // We'll need to maintain a list of config keys or use a different approach + // For now, return empty array with a note that this is a limitation + // In production, you might want to maintain a separate list of config keys + + let result: [String: Any] = [ + "configs": configs + ] + + print("DNP-PLUGIN: Found \(configs.count) config(s) (Note: UserDefaults doesn't support key enumeration)") + + call.resolve(result) + } + + /** + * Update config + * + * Updates an existing configuration value. + * + * Equivalent to Android's updateConfig method. + */ + @objc func updateConfig(_ call: CAPPluginCall) { + guard let key = call.getString("key") else { + call.reject("Config key is required") + return + } + + guard let value = call.getString("value") else { + call.reject("Config value is required") + return + } + + let options = call.getObject("options") + let timesafariDid = options?["timesafariDid"] as? String + + print("DNP-PLUGIN: Updating config: key=\(key), did=\(timesafariDid ?? "none")") + + // Build config key (include DID if provided) + let configKey = timesafariDid != nil ? "\(key)_\(timesafariDid!)" : key + let fullKey = "DailyNotificationConfig_\(configKey)" + + // Check if config exists + guard UserDefaults.standard.string(forKey: fullKey) != nil else { + call.reject("Config not found") + return + } + + // Update config value (store as JSON string) + do { + // Try to parse value as JSON, if it fails, store as plain string + let configValue: [String: Any] + if let jsonData = value.data(using: .utf8), + let json = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any] { + configValue = json + } else { + // Store as plain string value + configValue = ["value": value] + } + + let configData = try JSONSerialization.data(withJSONObject: configValue, options: []) + let configString = String(data: configData, encoding: .utf8) ?? "{}" + + UserDefaults.standard.set(configString, forKey: fullKey) + UserDefaults.standard.synchronize() + + print("DNP-PLUGIN: Config updated successfully") + call.resolve(configValue) + } catch { + print("DNP-PLUGIN: Failed to update config: \(error)") + call.reject("Failed to update config: \(error.localizedDescription)") + } + } + + /** + * Delete config + * + * Deletes a configuration by key. + * + * Equivalent to Android's deleteConfig method. + */ + @objc func deleteConfig(_ call: CAPPluginCall) { + guard let key = call.getString("key") else { + call.reject("Config key is required") + return + } + + let options = call.getObject("options") + let timesafariDid = options?["timesafariDid"] as? String + + print("DNP-PLUGIN: Deleting config: key=\(key), did=\(timesafariDid ?? "none")") + + // Build config key (include DID if provided) + let configKey = timesafariDid != nil ? "\(key)_\(timesafariDid!)" : key + let fullKey = "DailyNotificationConfig_\(configKey)" + + // Check if config exists + guard UserDefaults.standard.string(forKey: fullKey) != nil else { + call.reject("Config not found") + return + } + + UserDefaults.standard.removeObject(forKey: fullKey) + UserDefaults.standard.synchronize() + + print("DNP-PLUGIN: Config deleted successfully") + call.resolve() + } + // MARK: - Permission Methods /**