Fix iOS build errors and test app setup

- Fix async/await usage in background fetch handler
- Fix Core Data metadata access errors
- Replace SQLITE_TRANSIENT with nil for Swift compatibility
- Fix PermissionStatus interface and type casts in test app
- Add iOS setup documentation to BUILDING.md
- Update iOS sync workflow to handle Podfile regeneration

Resolves all iOS compilation errors and improves test app setup process.
This commit is contained in:
Jose Olarte III
2025-12-30 12:35:10 +08:00
parent 36e15633be
commit 9565191101
9 changed files with 280 additions and 82 deletions

View File

@@ -263,12 +263,12 @@ class DailyNotificationDatabase {
return
}
sqlite3_bind_text(stmt, 1, (content.id as NSString).utf8String, -1, SQLITE_TRANSIENT)
sqlite3_bind_text(stmt, 2, (json as NSString).utf8String, -1, SQLITE_TRANSIENT)
sqlite3_bind_text(stmt, 1, (content.id as NSString).utf8String, -1, nil)
sqlite3_bind_text(stmt, 2, (json as NSString).utf8String, -1, nil)
sqlite3_bind_int64(stmt, 3, sqlite3_int64(content.fetchedAt))
if let etag = content.etag {
sqlite3_bind_text(stmt, 4, (etag as NSString).utf8String, -1, SQLITE_TRANSIENT)
sqlite3_bind_text(stmt, 4, (etag as NSString).utf8String, -1, nil)
} else {
sqlite3_bind_null(stmt, 4)
}
@@ -310,7 +310,7 @@ class DailyNotificationDatabase {
return
}
sqlite3_bind_text(stmt, 1, (id as NSString).utf8String, -1, SQLITE_TRANSIENT)
sqlite3_bind_text(stmt, 1, (id as NSString).utf8String, -1, nil)
if sqlite3_step(stmt) != SQLITE_DONE {
print("\(Self.TAG): deleteNotificationContent step failed: \(String(cString: sqlite3_errmsg(db)))")

View File

@@ -261,17 +261,8 @@ class PersistenceController {
description?.shouldMigrateStoreAutomatically = true
description?.shouldInferMappingModelAutomatically = true
// Set initial schema version metadata (for new stores)
if !inMemory {
var metadata = description?.metadata ?? [:]
if metadata["schema_version"] == nil {
metadata["schema_version"] = PersistenceController.SCHEMA_VERSION
description?.metadata = metadata
}
}
var loadError: Error? = nil
tempContainer?.loadPersistentStores { description, error in
tempContainer?.loadPersistentStores { storeDescription, error in
if let error = error as NSError? {
loadError = error
print("DNP-PLUGIN: CoreData store load error: \(error.localizedDescription)")
@@ -281,7 +272,19 @@ class PersistenceController {
}
} else {
print("DNP-PLUGIN: CoreData store loaded successfully")
print("DNP-PLUGIN: Store URL: \(description.url?.absoluteString ?? "unknown")")
print("DNP-PLUGIN: Store URL: \(storeDescription.url?.absoluteString ?? "unknown")")
// Set initial schema version metadata (for new stores)
// Metadata must be set using the coordinator after the store is loaded
if !inMemory,
let coordinator = tempContainer?.persistentStoreCoordinator,
let store = coordinator.persistentStores.first,
let metadata = store.metadata,
metadata["schema_version"] == nil {
var newMetadata = metadata
newMetadata["schema_version"] = PersistenceController.SCHEMA_VERSION
coordinator.setMetadata(newMetadata, for: store)
}
}
}
@@ -373,7 +376,13 @@ class PersistenceController {
return
}
let currentVersion = store.metadata["schema_version"] as? Int ?? 1
// store.metadata is optional, so we need to unwrap it
guard let metadata = store.metadata else {
print("DNP-PLUGIN: Store metadata is nil, using default schema version")
return
}
let currentVersion = metadata["schema_version"] as? Int ?? 1
let expectedVersion = PersistenceController.SCHEMA_VERSION
if currentVersion != expectedVersion {
@@ -381,9 +390,13 @@ class PersistenceController {
print("DNP-PLUGIN: CoreData auto-migration will handle schema changes")
// Update metadata for future reference (does not trigger migration)
var metadata = store.metadata
metadata["schema_version"] = expectedVersion
// Note: Metadata persists on next store save
// Use the coordinator to set metadata
if let coordinator = container?.persistentStoreCoordinator {
var newMetadata = metadata
newMetadata["schema_version"] = expectedVersion
coordinator.setMetadata(newMetadata, for: store)
// Note: Metadata persists on next store save
}
} else {
print("DNP-PLUGIN: Schema version verified: \(currentVersion)")
}

View File

@@ -419,58 +419,58 @@ public class DailyNotificationPlugin: CAPPlugin {
// Phase 3: Check for JWT-signed fetcher configuration
// If native fetcher is configured, use it; otherwise fall back to dummy content
let nativeFetcherConfig = UserDefaults.standard.string(forKey: "native_fetcher_config")
let content: NotificationContent
if let configJson = nativeFetcherConfig,
let configData = configJson.data(using: .utf8),
let config = try? JSONSerialization.jsonObject(with: configData) as? [String: Any],
let apiBaseUrl = config["apiBaseUrl"] as? String,
let activeDid = config["activeDid"] as? String,
let jwtToken = config["jwtToken"] as? String {
// Phase 3: JWT-signed fetcher is configured - attempt HTTP fetch
print("DNP-FETCH: Using JWT-signed fetcher (apiBaseUrl=\(apiBaseUrl), activeDid=\(activeDid.prefix(30))...)")
// Save content to storage via state actor (thread-safe)
Task {
let content: NotificationContent
// Attempt to fetch content from TimeSafari API
// Note: This is a minimal implementation - can be extended with full API client
do {
let fetchedContent = try await fetchContentFromAPI(
apiBaseUrl: apiBaseUrl,
activeDid: activeDid,
jwtToken: jwtToken
)
content = fetchedContent
print("DNP-FETCH: Successfully fetched content from API")
} catch {
// Fallback to dummy content on fetch failure
print("DNP-FETCH: API fetch failed (\(error.localizedDescription)), using fallback content")
if let configJson = nativeFetcherConfig,
let configData = configJson.data(using: .utf8),
let config = try? JSONSerialization.jsonObject(with: configData) as? [String: Any],
let apiBaseUrl = config["apiBaseUrl"] as? String,
let activeDid = config["activeDid"] as? String,
let jwtToken = config["jwtToken"] as? String {
// Phase 3: JWT-signed fetcher is configured - attempt HTTP fetch
print("DNP-FETCH: Using JWT-signed fetcher (apiBaseUrl=\(apiBaseUrl), activeDid=\(activeDid.prefix(30))...)")
// Attempt to fetch content from TimeSafari API
// Note: This is a minimal implementation - can be extended with full API client
do {
let fetchedContent = try await fetchContentFromAPI(
apiBaseUrl: apiBaseUrl,
activeDid: activeDid,
jwtToken: jwtToken
)
content = fetchedContent
print("DNP-FETCH: Successfully fetched content from API")
} catch {
// Fallback to dummy content on fetch failure
print("DNP-FETCH: API fetch failed (\(error.localizedDescription)), using fallback content")
content = NotificationContent(
id: "fallback_\(Date().timeIntervalSince1970)",
title: "Daily Update",
body: "Your daily notification is ready",
scheduledTime: Int64(Date().timeIntervalSince1970 * 1000) + (5 * 60 * 1000),
fetchedAt: Int64(Date().timeIntervalSince1970 * 1000),
url: nil,
payload: ["fetchError": error.localizedDescription],
etag: nil
)
}
} else {
// Fallback: Dummy content fetch (no network)
print("DNP-FETCH: Using dummy content (native fetcher not configured)")
content = NotificationContent(
id: "fallback_\(Date().timeIntervalSince1970)",
id: "dummy_\(Date().timeIntervalSince1970)",
title: "Daily Update",
body: "Your daily notification is ready",
scheduledTime: Int64(Date().timeIntervalSince1970 * 1000) + (5 * 60 * 1000),
fetchedAt: Int64(Date().timeIntervalSince1970 * 1000),
url: nil,
payload: ["fetchError": error.localizedDescription],
payload: nil,
etag: nil
)
}
} else {
// Fallback: Dummy content fetch (no network)
print("DNP-FETCH: Using dummy content (native fetcher not configured)")
content = NotificationContent(
id: "dummy_\(Date().timeIntervalSince1970)",
title: "Daily Update",
body: "Your daily notification is ready",
scheduledTime: Int64(Date().timeIntervalSince1970 * 1000) + (5 * 60 * 1000),
fetchedAt: Int64(Date().timeIntervalSince1970 * 1000),
url: nil,
payload: nil,
etag: nil
)
}
// Save content to storage via state actor (thread-safe)
Task {
do {
// Use the content (either from JWT fetcher or dummy)
if #available(iOS 13.0, *) {
@@ -513,11 +513,16 @@ public class DailyNotificationPlugin: CAPPlugin {
// Phase 3.3: Schedule next background task
// Calculate next fetch time based on notification schedule
if let nextScheduledTime = self.getNextScheduledNotificationTime() {
self.scheduleBackgroundFetch(scheduledTime: nextScheduledTime)
print("DNP-FETCH: Next background fetch scheduled")
if let scheduler = self.scheduler {
let nextScheduledTime = await scheduler.getNextNotificationTime()
if let nextTime = nextScheduledTime {
self.scheduleBackgroundFetch(scheduledTime: nextTime)
print("DNP-FETCH: Next background fetch scheduled")
} else {
print("DNP-FETCH: No future notifications found, skipping next task schedule")
}
} else {
print("DNP-FETCH: No future notifications found, skipping next task schedule")
print("DNP-FETCH: Scheduler not available, skipping next task schedule")
}
guard !taskCompleted else { return }
@@ -575,10 +580,13 @@ public class DailyNotificationPlugin: CAPPlugin {
// Phase 3.3: Schedule next background task if needed
// For notify task, schedule next occurrence if applicable
if let nextScheduledTime = self.getNextScheduledNotificationTime() {
// Calculate next notify task time (if applicable)
// Note: Notify tasks are typically scheduled less frequently than fetch tasks
print("DNP-NOTIFY: Next notification scheduled at \(nextScheduledTime)")
if let scheduler = self.scheduler {
let nextScheduledTime = await scheduler.getNextNotificationTime()
if let nextTime = nextScheduledTime {
// Calculate next notify task time (if applicable)
// Note: Notify tasks are typically scheduled less frequently than fetch tasks
print("DNP-NOTIFY: Next notification scheduled at \(nextTime)")
}
}
guard !taskCompleted else { return }
@@ -1091,7 +1099,7 @@ public class DailyNotificationPlugin: CAPPlugin {
scheduler: scheduler,
storage: self.storage,
stateActor: await self.stateActor,
scheduleBackgroundFetch: { [weak self] scheduledTime in
scheduleBackgroundFetch: { [weak self] (scheduledTime: Int64) -> Void in
self?.scheduleBackgroundFetch(scheduledTime: scheduledTime)
}
)
@@ -1532,8 +1540,8 @@ public class DailyNotificationPlugin: CAPPlugin {
// User must check in Settings app
// Delegate storage access to storage service
let lastFetchExecution = storage?.getLastSuccessfulRun() ?? NSNull()
let lastNotifyExecution = storage?.getLastNotifyExecution() ?? NSNull()
let lastFetchExecution: Any = storage?.getLastSuccessfulRun() ?? NSNull()
let lastNotifyExecution: Any = storage?.getLastNotifyExecution() ?? NSNull()
let result: [String: Any] = [
"fetchTaskRegistered": true, // Assumed registered if setupBackgroundTasks() was called
@@ -1630,6 +1638,7 @@ public class DailyNotificationPlugin: CAPPlugin {
// Get channelId from call (optional, for API parity with Android)
// iOS doesn't have per-channel control, so check app-wide notification authorization
let channelId = call.getString("channelId") ?? "default"
Task {
// Delegate to scheduler for permission status check
let status = await scheduler.checkPermissionStatus()
@@ -1677,9 +1686,6 @@ public class DailyNotificationPlugin: CAPPlugin {
}
}
}
call.reject("Invalid settings URL")
}
}
/**
* Update notification settings