feat(ios): implement Phase 1 permission methods and fix build issues

Implement checkPermissionStatus() and requestNotificationPermissions()
methods for iOS plugin, matching Android functionality. Fix compilation
errors across plugin files and add comprehensive build/test infrastructure.

Key Changes:
- Add checkPermissionStatus() and requestNotificationPermissions() methods
- Fix 13+ categories of Swift compilation errors (type conversions, logger
  API, access control, async/await, etc.)
- Create DailyNotificationScheduler, DailyNotificationStorage,
  DailyNotificationStateActor, and DailyNotificationErrorCodes components
- Fix CoreData initialization to handle missing model gracefully for Phase 1
- Add iOS test app build script with simulator auto-detection
- Update directive with lessons learned from build and permission work

Build Status:  BUILD SUCCEEDED
Test App:  Ready for iOS Simulator testing

Files Modified:
- doc/directives/0003-iOS-Android-Parity-Directive.md (lessons learned)
- ios/Plugin/DailyNotificationPlugin.swift (Phase 1 methods)
- ios/Plugin/DailyNotificationModel.swift (CoreData fix)
- 11+ other plugin files (compilation fixes)

Files Added:
- ios/Plugin/DailyNotificationScheduler.swift
- ios/Plugin/DailyNotificationStorage.swift
- ios/Plugin/DailyNotificationStateActor.swift
- ios/Plugin/DailyNotificationErrorCodes.swift
- scripts/build-ios-test-app.sh
- scripts/setup-ios-test-app.sh
- test-apps/ios-test-app/ (full test app)
- Multiple Phase 1 documentation files
This commit is contained in:
Server
2025-11-13 05:14:24 -08:00
parent 2d84ae29ba
commit 5844b92e18
61 changed files with 9676 additions and 356 deletions

View File

@@ -15,19 +15,68 @@ import Foundation
* This class encapsulates all the information needed for a notification
* including scheduling, content, and metadata.
*/
class NotificationContent {
class NotificationContent: Codable {
// MARK: - Properties
let id: String
let title: String?
let body: String?
let scheduledTime: TimeInterval // milliseconds since epoch
let fetchedAt: TimeInterval // milliseconds since epoch
let scheduledTime: Int64 // milliseconds since epoch (matches Android long)
let fetchedAt: Int64 // milliseconds since epoch (matches Android long)
let url: String?
let payload: [String: Any]?
let etag: String?
// MARK: - Codable Support
enum CodingKeys: String, CodingKey {
case id
case title
case body
case scheduledTime
case fetchedAt
case url
case payload
case etag
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
title = try container.decodeIfPresent(String.self, forKey: .title)
body = try container.decodeIfPresent(String.self, forKey: .body)
scheduledTime = try container.decode(Int64.self, forKey: .scheduledTime)
fetchedAt = try container.decode(Int64.self, forKey: .fetchedAt)
url = try container.decodeIfPresent(String.self, forKey: .url)
// payload is encoded as JSON string
if let payloadString = try? container.decodeIfPresent(String.self, forKey: .payload),
let payloadData = payloadString.data(using: .utf8),
let payloadDict = try? JSONSerialization.jsonObject(with: payloadData) as? [String: Any] {
payload = payloadDict
} else {
payload = nil
}
etag = try container.decodeIfPresent(String.self, forKey: .etag)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encodeIfPresent(title, forKey: .title)
try container.encodeIfPresent(body, forKey: .body)
try container.encode(scheduledTime, forKey: .scheduledTime)
try container.encode(fetchedAt, forKey: .fetchedAt)
try container.encodeIfPresent(url, forKey: .url)
// Encode payload as JSON string
if let payload = payload,
let payloadData = try? JSONSerialization.data(withJSONObject: payload),
let payloadString = String(data: payloadData, encoding: .utf8) {
try container.encode(payloadString, forKey: .payload)
}
try container.encodeIfPresent(etag, forKey: .etag)
}
// MARK: - Initialization
/**
@@ -45,8 +94,8 @@ class NotificationContent {
init(id: String,
title: String?,
body: String?,
scheduledTime: TimeInterval,
fetchedAt: TimeInterval,
scheduledTime: Int64,
fetchedAt: Int64,
url: String?,
payload: [String: Any]?,
etag: String?) {
@@ -69,7 +118,7 @@ class NotificationContent {
* @return Scheduled time as Date object
*/
func getScheduledTimeAsDate() -> Date {
return Date(timeIntervalSince1970: scheduledTime / 1000)
return Date(timeIntervalSince1970: Double(scheduledTime) / 1000.0)
}
/**
@@ -78,7 +127,7 @@ class NotificationContent {
* @return Fetched time as Date object
*/
func getFetchedTimeAsDate() -> Date {
return Date(timeIntervalSince1970: fetchedAt / 1000)
return Date(timeIntervalSince1970: Double(fetchedAt) / 1000.0)
}
/**
@@ -113,7 +162,8 @@ class NotificationContent {
* @return true if scheduled time is in the future
*/
func isInTheFuture() -> Bool {
return scheduledTime > Date().timeIntervalSince1970 * 1000
let currentTime = Int64(Date().timeIntervalSince1970 * 1000)
return scheduledTime > currentTime
}
/**
@@ -122,7 +172,7 @@ class NotificationContent {
* @return Age in seconds at scheduled time
*/
func getAgeAtScheduledTime() -> TimeInterval {
return (scheduledTime - fetchedAt) / 1000
return Double(scheduledTime - fetchedAt) / 1000.0
}
/**
@@ -150,9 +200,26 @@ class NotificationContent {
* @return NotificationContent instance
*/
static func fromDictionary(_ dict: [String: Any]) -> NotificationContent? {
guard let id = dict["id"] as? String,
let scheduledTime = dict["scheduledTime"] as? TimeInterval,
let fetchedAt = dict["fetchedAt"] as? TimeInterval else {
guard let id = dict["id"] as? String else {
return nil
}
// Handle both Int64 and TimeInterval (Double) for backward compatibility
let scheduledTime: Int64
if let time = dict["scheduledTime"] as? Int64 {
scheduledTime = time
} else if let time = dict["scheduledTime"] as? Double {
scheduledTime = Int64(time)
} else {
return nil
}
let fetchedAt: Int64
if let time = dict["fetchedAt"] as? Int64 {
fetchedAt = time
} else if let time = dict["fetchedAt"] as? Double {
fetchedAt = Int64(time)
} else {
return nil
}