feat: complete Tasks 21-28 — backend integration, security hardening, UI tests & CI
- Add Apple Sign-In backend (JWKS verification, account linking, session management) - Implement push notification deep linking with NotificationDeepLinkRouter - Add jailbreak detection, runtime integrity monitoring, secure enclave service - Implement OAuth social login, token refresh, and secure logout flows - Add image caching (memory/disk), optimizer, upload queue, async semaphore - Implement notification analytics, type preferences, and category setup - Expand UI test suite with UITestBase, accessibility, auth flow, performance tests - Add CI pipeline for iOS UI tests (3 device sizes) and performance benchmarks - Restructure Xcode project to manual groups with KordantWidgets target - Add SwiftLint, Swift Collections/Algorithms/GoogleSignIn dependencies - Update project.yml for XcodeGen with new targets and configurations
This commit is contained in:
105
iOS/Sources/Shared/WidgetData.swift
Normal file
105
iOS/Sources/Shared/WidgetData.swift
Normal file
@@ -0,0 +1,105 @@
|
||||
import Foundation
|
||||
|
||||
/// Data model shared between the main app and widget extension via App Group UserDefaults.
|
||||
struct WidgetData: Codable, Equatable {
|
||||
let threatScore: Double
|
||||
let recentAlerts: [WidgetAlert]
|
||||
let alertCount: Int
|
||||
let unreadCount: Int
|
||||
let criticalCount: Int
|
||||
let exposureCount: Int
|
||||
let lastUpdated: Date
|
||||
|
||||
static let placeholder = WidgetData(
|
||||
threatScore: 0.25,
|
||||
recentAlerts: WidgetAlert.placeholders,
|
||||
alertCount: 5,
|
||||
unreadCount: 3,
|
||||
criticalCount: 1,
|
||||
exposureCount: 2,
|
||||
lastUpdated: Date()
|
||||
)
|
||||
|
||||
static let unavailable = WidgetData(
|
||||
threatScore: 0,
|
||||
recentAlerts: [],
|
||||
alertCount: 0,
|
||||
unreadCount: 0,
|
||||
criticalCount: 0,
|
||||
exposureCount: 0,
|
||||
lastUpdated: Date()
|
||||
)
|
||||
|
||||
var threatLevel: ThreatLevel {
|
||||
let pct = threatScore
|
||||
if pct >= 0.7 { return .critical }
|
||||
if pct >= 0.4 { return .high }
|
||||
if pct >= 0.2 { return .medium }
|
||||
return .low
|
||||
}
|
||||
}
|
||||
|
||||
enum ThreatLevel: String, Codable {
|
||||
case low, medium, high, critical
|
||||
|
||||
var label: String { rawValue.capitalized }
|
||||
}
|
||||
|
||||
struct WidgetAlert: Codable, Identifiable, Equatable {
|
||||
let id: String
|
||||
let title: String
|
||||
let message: String
|
||||
let severity: String
|
||||
let type: String
|
||||
let createdAt: Date?
|
||||
|
||||
var severityEnum: AlertSeverity {
|
||||
AlertSeverity(rawValue: severity) ?? .low
|
||||
}
|
||||
|
||||
var typeEnum: AlertType {
|
||||
AlertType(rawValue: type) ?? .exposure
|
||||
}
|
||||
|
||||
static let placeholders: [WidgetAlert] = [
|
||||
WidgetAlert(
|
||||
id: "placeholder-1",
|
||||
title: "Data Breach Detected",
|
||||
message: "Your email was found in a recent data breach.",
|
||||
severity: "critical",
|
||||
type: "breach",
|
||||
createdAt: Date()
|
||||
),
|
||||
WidgetAlert(
|
||||
id: "placeholder-2",
|
||||
title: "New Exposure Found",
|
||||
message: "Personal information exposed on a public forum.",
|
||||
severity: "high",
|
||||
type: "exposure",
|
||||
createdAt: Date().addingTimeInterval(-3600)
|
||||
),
|
||||
WidgetAlert(
|
||||
id: "placeholder-3",
|
||||
title: "Voice Match Detected",
|
||||
message: "Suspicious call pattern flagged.",
|
||||
severity: "medium",
|
||||
type: "voiceMatch",
|
||||
createdAt: Date().addingTimeInterval(-7200)
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
enum AlertSeverity: String, Codable {
|
||||
case low = "low"
|
||||
case medium = "medium"
|
||||
case high = "high"
|
||||
case critical = "critical"
|
||||
}
|
||||
|
||||
enum AlertType: String, Codable {
|
||||
case exposure = "exposure"
|
||||
case breach = "breach"
|
||||
case login = "login"
|
||||
case voiceMatch = "voiceMatch"
|
||||
case removal = "removal"
|
||||
}
|
||||
46
iOS/Sources/Shared/WidgetDataManager.swift
Normal file
46
iOS/Sources/Shared/WidgetDataManager.swift
Normal file
@@ -0,0 +1,46 @@
|
||||
import Foundation
|
||||
|
||||
/// Reads and writes WidgetData from the shared App Group UserDefaults container.
|
||||
/// Compiled into both the main app and widget extension targets.
|
||||
final class WidgetDataManager {
|
||||
static let shared = WidgetDataManager()
|
||||
|
||||
private let suiteName = "group.com.frenocorp.kordant"
|
||||
private let dataKey = "com.frenocorp.kordant.widgetData"
|
||||
|
||||
private init() {}
|
||||
|
||||
/// Persist widget data to the shared container.
|
||||
func save(_ data: WidgetData) {
|
||||
guard let defaults = UserDefaults(suiteName: suiteName) else {
|
||||
assertionFailure("WidgetDataManager: Unable to access App Group UserDefaults")
|
||||
return
|
||||
}
|
||||
do {
|
||||
let encoded = try JSONEncoder().encode(data)
|
||||
defaults.set(encoded, forKey: dataKey)
|
||||
} catch {
|
||||
assertionFailure("WidgetDataManager: Failed to encode widget data: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
/// Load widget data from the shared container.
|
||||
/// Returns `nil` if no data has been written yet.
|
||||
func load() -> WidgetData? {
|
||||
guard let defaults = UserDefaults(suiteName: suiteName),
|
||||
let data = defaults.data(forKey: dataKey) else {
|
||||
return nil
|
||||
}
|
||||
do {
|
||||
return try JSONDecoder().decode(WidgetData.self, from: data)
|
||||
} catch {
|
||||
assertionFailure("WidgetDataManager: Failed to decode widget data: \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove all stored widget data (e.g. on logout).
|
||||
func clear() {
|
||||
UserDefaults(suiteName: suiteName)?.removeObject(forKey: dataKey)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user