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

@@ -540,6 +540,10 @@ A "successful run" is defined as: BGTask handler invoked, content fetch complete
| `cancelAllNotifications()` | `cancelAllNotifications(): Promise<void>` | `@objc func cancelAllNotifications(_ call: CAPPluginCall)` | `DailyNotificationPlugin.swift` | 1 | ✅ Complete |
| `getNotificationStatus()` | `getNotificationStatus(): Promise<NotificationStatus>` | `@objc func getNotificationStatus(_ call: CAPPluginCall)` | `DailyNotificationPlugin.swift` | 1 | ✅ Complete |
| `updateSettings()` | `updateSettings(settings: NotificationSettings): Promise<void>` | `@objc func updateSettings(_ call: CAPPluginCall)` | `DailyNotificationPlugin.swift` | 1 | ✅ Complete |
| `checkPermissionStatus()` | `checkPermissionStatus(): Promise<PermissionStatusResult>` | `@objc func checkPermissionStatus(_ call: CAPPluginCall)` | `DailyNotificationPlugin.swift` | 1 | ✅ Complete |
| `requestNotificationPermissions()` | `requestNotificationPermissions(): Promise<PermissionStatus>` | `@objc func requestNotificationPermissions(_ call: CAPPluginCall)` | `DailyNotificationPlugin.swift` | 1 | ✅ Complete |
| `isChannelEnabled()` | `isChannelEnabled(channelId?: string): Promise<{ enabled: boolean; channelId: string }>` | `@objc func isChannelEnabled(_ call: CAPPluginCall)` | `DailyNotificationPlugin.swift` | 1 | ✅ Complete |
| `openChannelSettings()` | `openChannelSettings(channelId?: string): Promise<void>` | `@objc func openChannelSettings(_ call: CAPPluginCall)` | `DailyNotificationPlugin.swift` | 1 | ✅ Complete |
| `getBatteryStatus()` | `getBatteryStatus(): Promise<BatteryStatus>` | `@objc func getBatteryStatus(_ call: CAPPluginCall)` | `DailyNotificationPlugin.swift` | 2 | ❌ Missing |
| `requestBatteryOptimizationExemption()` | `requestBatteryOptimizationExemption(): Promise<void>` | `@objc func requestBatteryOptimizationExemption(_ call: CAPPluginCall)` | `DailyNotificationPlugin.swift` | 2 | ❌ Missing |
| `setAdaptiveScheduling()` | `setAdaptiveScheduling(options: { enabled: boolean }): Promise<void>` | `@objc func setAdaptiveScheduling(_ call: CAPPluginCall)` | `DailyNotificationPlugin.swift` | 2 | ❌ Missing |
@@ -612,6 +616,10 @@ A "successful run" is defined as: BGTask handler invoked, content fetch complete
- [x] `cancelAllNotifications()` - Cancel all notifications ✅
- [x] `getNotificationStatus()` - Status retrieval ✅
- [x] `updateSettings(settings: NotificationSettings)` - Settings update ✅
- [x] `checkPermissionStatus()` - Permission status check ✅
- [x] `requestNotificationPermissions()` - Request permissions ✅
- [x] `isChannelEnabled(channelId?)` - Channel enabled check (iOS: app-wide) ✅
- [x] `openChannelSettings(channelId?)` - Open channel settings (iOS: app Settings) ✅
### Power Management Methods (Phase 2)
@@ -703,6 +711,11 @@ A "successful run" is defined as: BGTask handler invoked, content fetch complete
- Background refresh tasks: ~30 seconds execution time
- Processing tasks: Variable, depends on system resources
- **Strategy:** Efficient processing, immediate next-schedule after completion
- **Simulator Limitation:** BGTaskScheduler doesn't work reliably on simulator (Code=1: notPermitted)
- This is **expected behavior** - background fetch scheduling will fail on simulator
- Notifications will still be delivered, just without prefetch
- Real device testing required to verify background fetch works
- Error handling logs clear message that simulator limitation is expected
### iOS Storage Options
@@ -1387,6 +1400,42 @@ scripts/
**Status:** ✅ **METHODS IMPLEMENTED** (2025-11-13)
- `checkPermissionStatus()` - Returns current notification permission status
- `requestNotificationPermissions()` - Requests notification permissions (shows system dialog if `.notDetermined`)
- `isChannelEnabled(channelId?)` - Checks if notifications are enabled (iOS: app-wide check, not per-channel)
- `openChannelSettings(channelId?)` - Opens notification settings (iOS: opens app Settings, not per-channel)
- `scheduleDailyNotification(options)` - Schedules daily notification (✅ working, BGTaskScheduler prefetch fails on simulator - expected)
### 2025-11-13: scheduleDailyNotification Implementation
**Decision:** Implement `scheduleDailyNotification` with proper parameter handling
**Rationale:** Test app needs to schedule notifications for testing
**Status:** ✅ Complete
**Implementation Details:**
1. **Parameter Reading Fix:**
- **Issue:** iOS code was looking for `call.getObject("options")` but Capacitor passes parameters directly
- **Fix:** Changed to read parameters directly from `call` (matching Android pattern)
- **Files Affected:** `ios/Plugin/DailyNotificationPlugin.swift`
- **Lesson:** Capacitor passes the options object directly as call data, not wrapped in "options" key
2. **BGTaskScheduler Simulator Limitation:**
- **Issue:** Background fetch scheduling fails on simulator with `BGTaskSchedulerErrorDomain Code=1` (notPermitted)
- **Behavior:** This is **expected** on simulator - BGTaskScheduler doesn't work reliably in simulators
- **Solution:** Improved error handling to log clear message that this is expected on simulator
- **Impact:** Notification scheduling still works; prefetch won't run on simulator but will work on real devices
- **Files Affected:** `ios/Plugin/DailyNotificationPlugin.swift`
- **Lesson:** BGTaskScheduler requires real device or Background App Refresh enabled; simulator limitations are normal
3. **Notification Scheduling:**
- ✅ Notification scheduling works correctly
- ✅ Storage saves notification content
- ✅ Background fetch scheduling attempted (fails on simulator, works on device)
- ✅ Error handling doesn't fail notification scheduling if background fetch fails
**Testing Notes:**
- Notification scheduling verified working on simulator
- Background fetch error is expected and doesn't prevent notification delivery
- Real device testing required to verify background fetch works
---

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))