fix(ios): fix scheduleDailyNotification parameter handling and BGTaskScheduler error handling

Fixed scheduleDailyNotification to read parameters directly from CAPPluginCall
(matching Android pattern) instead of looking for wrapped "options" object.
Improved BGTaskScheduler error handling to clearly indicate simulator limitations.

Changes:
- Read parameters directly from call (call.getString("time"), etc.) instead of
  call.getObject("options") - Capacitor passes options object directly as call data
- Improved BGTaskScheduler error handling with clear simulator limitation message
- Added priority parameter extraction (was missing)
- Error handling doesn't fail notification scheduling if background fetch fails

BGTaskScheduler Simulator Limitation:
- BGTaskSchedulerErrorDomain Code=1 (notPermitted) is expected on simulator
- Background fetch scheduling fails on simulator but works on real devices
- Notification scheduling still works correctly; prefetch won't run on simulator
- Error messages now clearly indicate this is expected behavior

Result: scheduleDailyNotification now works correctly. Notification scheduling
verified working on simulator. Background fetch error is expected and documented.

Files modified:
- ios/Plugin/DailyNotificationPlugin.swift: Parameter reading fix, error handling
- doc/directives/0003-iOS-Android-Parity-Directive.md: Implementation details documented
This commit is contained in:
Server
2025-11-13 23:51:23 -08:00
parent ed25b1385a
commit 88aa34b33f
2 changed files with 162 additions and 15 deletions

View File

@@ -863,15 +863,9 @@ public class DailyNotificationPlugin: CAPPlugin {
return
}
guard let options = call.getObject("options") else {
let error = DailyNotificationErrorCodes.missingParameter("options")
let errorMessage = error["message"] as? String ?? "Unknown error"
let errorCode = error["error"] as? String ?? "unknown_error"
call.reject(errorMessage, errorCode)
return
}
guard let time = options["time"] as? String, !time.isEmpty else {
// Capacitor passes the options object directly as call data
// Read parameters directly from call (matching Android implementation)
guard let time = call.getString("time"), !time.isEmpty else {
let error = DailyNotificationErrorCodes.missingParameter("time")
let errorMessage = error["message"] as? String ?? "Unknown error"
let errorCode = error["error"] as? String ?? "unknown_error"
@@ -893,11 +887,12 @@ public class DailyNotificationPlugin: CAPPlugin {
return
}
// Extract other parameters
let title = options["title"] as? String ?? "Daily Update"
let body = options["body"] as? String ?? "Your daily notification is ready"
let sound = options["sound"] as? Bool ?? true
let url = options["url"] as? String
// Extract other parameters (with defaults matching Android)
let title = call.getString("title") ?? "Daily Update"
let body = call.getString("body") ?? "Your daily notification is ready"
let sound = call.getBool("sound", true)
let url = call.getString("url")
let priority = call.getString("priority") ?? "default"
// Calculate scheduled time (next occurrence at specified hour:minute)
let scheduledTime = calculateNextScheduledTime(hour: hour, minute: minute)
@@ -1267,6 +1262,92 @@ public class DailyNotificationPlugin: CAPPlugin {
}
}
// MARK: - Channel Methods (iOS Parity with Android)
/**
* Check if notification channel is enabled
*
* iOS Note: iOS doesn't have per-channel control like Android. This method
* checks if notifications are authorized app-wide, which is the iOS equivalent.
*
* @param call Plugin call with optional channelId parameter
*/
@objc func isChannelEnabled(_ call: CAPPluginCall) {
NSLog("DNP-PLUGIN: isChannelEnabled called")
// Ensure scheduler is initialized
if scheduler == nil {
scheduler = DailyNotificationScheduler()
}
guard let scheduler = scheduler else {
let error = DailyNotificationErrorCodes.createErrorResponse(
code: DailyNotificationErrorCodes.PLUGIN_NOT_INITIALIZED,
message: "Plugin not initialized"
)
let errorMessage = error["message"] as? String ?? "Unknown error"
let errorCode = error["error"] as? String ?? "unknown_error"
call.reject(errorMessage, errorCode)
return
}
// Get channelId from call (optional, for API parity with Android)
let channelId = call.getString("channelId") ?? "default"
// iOS doesn't have per-channel control, so check app-wide notification authorization
Task {
let status = await scheduler.checkPermissionStatus()
let enabled = (status == .authorized || status == .provisional)
let result: [String: Any] = [
"enabled": enabled,
"channelId": channelId
]
DispatchQueue.main.async {
call.resolve(result)
}
}
}
/**
* Open notification channel settings
*
* iOS Note: iOS doesn't have per-channel settings. This method opens
* the app's notification settings in iOS Settings, which is the iOS equivalent.
*
* @param call Plugin call with optional channelId parameter
*/
@objc func openChannelSettings(_ call: CAPPluginCall) {
NSLog("DNP-PLUGIN: openChannelSettings called")
// Get channelId from call (optional, for API parity with Android)
let channelId = call.getString("channelId") ?? "default"
// iOS doesn't have per-channel settings, so open app-wide notification settings
if let settingsUrl = URL(string: UIApplication.openSettingsURLString) {
if UIApplication.shared.canOpenURL(settingsUrl) {
UIApplication.shared.open(settingsUrl) { success in
if success {
NSLog("DNP-PLUGIN: Opened iOS Settings for channel: %@", channelId)
DispatchQueue.main.async {
call.resolve()
}
} else {
NSLog("DNP-PLUGIN: Failed to open iOS Settings")
DispatchQueue.main.async {
call.reject("Failed to open settings")
}
}
}
} else {
call.reject("Cannot open settings URL")
}
} else {
call.reject("Invalid settings URL")
}
}
/**
* Update notification settings
*
@@ -1371,7 +1452,20 @@ public class DailyNotificationPlugin: CAPPlugin {
print("DNP-FETCH-SCHEDULE: Background fetch scheduled for \(fetchDate)")
} catch {
print("DNP-FETCH-SCHEDULE: Failed to schedule background fetch: \(error)")
// BGTaskScheduler errors are common on simulator (Code=1: notPermitted)
// This is expected behavior - simulators don't reliably support background tasks
// On real devices, this should work if Background App Refresh is enabled
let errorDescription = error.localizedDescription
if errorDescription.contains("BGTaskSchedulerErrorDomain") ||
errorDescription.contains("Code=1") ||
(error as NSError).domain == "BGTaskSchedulerErrorDomain" {
print("DNP-FETCH-SCHEDULE: Background fetch scheduling failed (expected on simulator): \(errorDescription)")
print("DNP-FETCH-SCHEDULE: Note: BGTaskScheduler requires real device or Background App Refresh enabled")
} else {
print("DNP-FETCH-SCHEDULE: Failed to schedule background fetch: \(error)")
}
// Don't fail notification scheduling if background fetch fails
// Notification will still be delivered, just without prefetch
}
}
}
@@ -1403,6 +1497,10 @@ public class DailyNotificationPlugin: CAPPlugin {
methods.append(CAPPluginMethod(name: "checkPermissionStatus", returnType: CAPPluginReturnPromise))
methods.append(CAPPluginMethod(name: "requestNotificationPermissions", returnType: CAPPluginReturnPromise))
// Channel methods (iOS parity with Android)
methods.append(CAPPluginMethod(name: "isChannelEnabled", returnType: CAPPluginReturnPromise))
methods.append(CAPPluginMethod(name: "openChannelSettings", returnType: CAPPluginReturnPromise))
// Reminder methods
methods.append(CAPPluginMethod(name: "scheduleDailyReminder", returnType: CAPPluginReturnPromise))
methods.append(CAPPluginMethod(name: "cancelDailyReminder", returnType: CAPPluginReturnPromise))