feat(ios): implement testAlarm method and fix plugin discovery
Add testAlarm() method to iOS plugin for quick notification testing. Fix plugin method discovery by registering testAlarm in CAPBridgedPlugin pluginMethods array. Add force-load code in AppDelegate to ensure plugin is discovered by Capacitor's objc_getClassList scan. Changes: - Add testAlarm() implementation in DailyNotificationPlugin.swift - Register testAlarm in pluginMethods array (required for Capacitor discovery) - Add force-load code in test app AppDelegate (matches working ios-test-app) - Add UNUserNotificationCenterDelegate to show notifications in foreground - Add test notification button to ScheduleView with immediate feedback - Add debug logging for method discovery and plugin loading Fixes issue where testAlarm was implemented but returned "UNIMPLEMENTED" because it wasn't registered in the pluginMethods array. Also ensures plugin class is loaded before Capacitor's discovery phase.
This commit is contained in:
@@ -11,6 +11,7 @@ import Capacitor
|
||||
import UserNotifications
|
||||
import BackgroundTasks
|
||||
import CoreData
|
||||
import ObjectiveC
|
||||
|
||||
/**
|
||||
* iOS implementation of Daily Notification Plugin
|
||||
@@ -82,6 +83,34 @@ public class DailyNotificationPlugin: CAPPlugin {
|
||||
|
||||
NSLog("DNP-DEBUG: DailyNotificationPlugin.load() completed - initialization done")
|
||||
print("DNP-PLUGIN: Daily Notification Plugin loaded on iOS")
|
||||
|
||||
// Debug: Log all available @objc methods for Capacitor discovery
|
||||
let methods = getObjCMethods()
|
||||
NSLog("DNP-DEBUG: Available @objc methods: \(methods.joined(separator: ", "))")
|
||||
print("DNP-DEBUG: Available @objc methods: \(methods.joined(separator: ", "))")
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug helper: Get all @objc methods for this class
|
||||
*/
|
||||
private func getObjCMethods() -> [String] {
|
||||
var methods: [String] = []
|
||||
var methodCount: UInt32 = 0
|
||||
let methodList = class_copyMethodList(type(of: self), &methodCount)
|
||||
|
||||
for i in 0..<Int(methodCount) {
|
||||
if let method = methodList?[i] {
|
||||
let selector = method_getName(method)
|
||||
let methodName = NSStringFromSelector(selector)
|
||||
// Filter for methods that look like plugin methods (take CAPPluginCall)
|
||||
if methodName.contains(":") && !methodName.hasPrefix("_") {
|
||||
methods.append(methodName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
free(methodList)
|
||||
return methods.sorted()
|
||||
}
|
||||
|
||||
// MARK: - Configuration Methods
|
||||
@@ -1120,6 +1149,129 @@ public class DailyNotificationPlugin: CAPPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test method: Schedule an alarm to fire in a few seconds
|
||||
* Useful for verifying alarm delivery works correctly
|
||||
*
|
||||
* @param call Plugin call with optional secondsFromNow (default: 5)
|
||||
* @returns Object with scheduled (boolean), secondsFromNow (number), and triggerAtMillis (number)
|
||||
*/
|
||||
@objc func testAlarm(_ call: CAPPluginCall) {
|
||||
NSLog("DNP-DEBUG: testAlarm() method CALLED - method is being invoked!")
|
||||
print("DNP-DEBUG: testAlarm() method CALLED - method is being invoked!")
|
||||
print("DNP-DEBUG: testAlarm call data: \(call.jsObjectRepresentation)")
|
||||
|
||||
guard let scheduler = scheduler else {
|
||||
NSLog("DNP-DEBUG: testAlarm() - scheduler is nil, rejecting")
|
||||
print("DNP-DEBUG: testAlarm() - scheduler is nil, rejecting")
|
||||
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 secondsFromNow parameter (default: 5)
|
||||
let secondsFromNow = call.getInt("secondsFromNow") ?? 5
|
||||
|
||||
// Ensure minimum of 1 second (iOS requirement)
|
||||
let validSeconds = max(1, secondsFromNow)
|
||||
|
||||
Task {
|
||||
do {
|
||||
// Check permissions first
|
||||
let permissionStatus = await notificationCenter.notificationSettings()
|
||||
if permissionStatus.authorizationStatus != .authorized && permissionStatus.authorizationStatus != .provisional {
|
||||
let error = DailyNotificationErrorCodes.createErrorResponse(
|
||||
code: DailyNotificationErrorCodes.NOTIFICATIONS_DENIED,
|
||||
message: "Notification permissions not granted"
|
||||
)
|
||||
let errorMessage = error["message"] as? String ?? "Unknown error"
|
||||
let errorCode = error["error"] as? String ?? "unknown_error"
|
||||
call.reject(errorMessage, errorCode)
|
||||
return
|
||||
}
|
||||
|
||||
// Create test notification content
|
||||
let notificationContent = UNMutableNotificationContent()
|
||||
notificationContent.title = "Test Notification"
|
||||
notificationContent.body = "This is a test notification scheduled \(validSeconds) seconds from now"
|
||||
notificationContent.sound = .default
|
||||
notificationContent.categoryIdentifier = "DAILY_NOTIFICATION"
|
||||
notificationContent.userInfo = [
|
||||
"notification_id": "test_\(Date().timeIntervalSince1970)",
|
||||
"is_test": true
|
||||
]
|
||||
|
||||
// Create time interval trigger (fires in X seconds)
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: TimeInterval(validSeconds), repeats: false)
|
||||
|
||||
// Create notification request with unique ID
|
||||
let notificationId = "test_alarm_\(Date().timeIntervalSince1970)"
|
||||
let request = UNNotificationRequest(
|
||||
identifier: notificationId,
|
||||
content: notificationContent,
|
||||
trigger: trigger
|
||||
)
|
||||
|
||||
// Schedule notification
|
||||
try await notificationCenter.add(request)
|
||||
|
||||
// Calculate trigger time in milliseconds
|
||||
let triggerAtMillis = Int64((Date().timeIntervalSince1970 + Double(validSeconds)) * 1000)
|
||||
|
||||
let result: [String: Any] = [
|
||||
"scheduled": true,
|
||||
"secondsFromNow": validSeconds,
|
||||
"triggerAtMillis": triggerAtMillis
|
||||
]
|
||||
|
||||
print("DNP-PLUGIN: Test alarm scheduled for \(validSeconds) seconds from now (triggerAtMillis=\(triggerAtMillis))")
|
||||
NSLog("DNP-DEBUG: testAlarm() - Successfully scheduled, resolving with result: \(result)")
|
||||
|
||||
DispatchQueue.main.async {
|
||||
NSLog("DNP-DEBUG: testAlarm() - Resolving call with result")
|
||||
call.resolve(result)
|
||||
}
|
||||
|
||||
} catch {
|
||||
NSLog("DNP-DEBUG: testAlarm() - Error caught: \(error)")
|
||||
print("DNP-PLUGIN: Error scheduling test alarm: \(error)")
|
||||
let errorResponse = DailyNotificationErrorCodes.createErrorResponse(
|
||||
code: DailyNotificationErrorCodes.SCHEDULING_FAILED,
|
||||
message: "Failed to schedule test alarm: \(error.localizedDescription)"
|
||||
)
|
||||
let errorMessage = errorResponse["message"] as? String ?? "Unknown error"
|
||||
let errorCode = errorResponse["error"] as? String ?? "unknown_error"
|
||||
NSLog("DNP-DEBUG: testAlarm() - Rejecting with error: \(errorMessage) (\(errorCode))")
|
||||
DispatchQueue.main.async {
|
||||
call.reject(errorMessage, errorCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug method: List all available plugin methods
|
||||
* Useful for verifying Capacitor method discovery
|
||||
*
|
||||
* @param call Plugin call
|
||||
*/
|
||||
@objc func listAvailableMethods(_ call: CAPPluginCall) {
|
||||
let methods = getObjCMethods()
|
||||
let result: [String: Any] = [
|
||||
"methods": methods,
|
||||
"count": methods.count,
|
||||
"testAlarmFound": methods.contains("testAlarm:")
|
||||
]
|
||||
NSLog("DNP-DEBUG: listAvailableMethods() - Found \(methods.count) methods")
|
||||
NSLog("DNP-DEBUG: testAlarm: found: \(methods.contains("testAlarm:"))")
|
||||
call.resolve(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last notification that was delivered
|
||||
*
|
||||
@@ -1943,6 +2095,7 @@ public class DailyNotificationPlugin: CAPPlugin {
|
||||
methods.append(CAPPluginMethod(name: "configure", returnType: CAPPluginReturnPromise))
|
||||
methods.append(CAPPluginMethod(name: "configureNativeFetcher", returnType: CAPPluginReturnPromise))
|
||||
methods.append(CAPPluginMethod(name: "scheduleDailyNotification", returnType: CAPPluginReturnPromise))
|
||||
methods.append(CAPPluginMethod(name: "testAlarm", returnType: CAPPluginReturnPromise))
|
||||
methods.append(CAPPluginMethod(name: "getLastNotification", returnType: CAPPluginReturnPromise))
|
||||
methods.append(CAPPluginMethod(name: "cancelAllNotifications", returnType: CAPPluginReturnPromise))
|
||||
methods.append(CAPPluginMethod(name: "getNotificationStatus", returnType: CAPPluginReturnPromise))
|
||||
|
||||
@@ -1,12 +1,52 @@
|
||||
import UIKit
|
||||
import Capacitor
|
||||
import BackgroundTasks
|
||||
import DailyNotificationPlugin
|
||||
import ObjectiveC
|
||||
import UserNotifications
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
NSLog("DNP-DEBUG: AppDelegate.application(_:didFinishLaunchingWithOptions:) called")
|
||||
|
||||
// CRITICAL: Force-load the plugin framework before Capacitor initializes
|
||||
// objc_getClassList may not include classes from frameworks that haven't been loaded yet
|
||||
// Even though NSClassFromString can find the class, Capacitor's discovery uses objc_getClassList
|
||||
// which only includes loaded classes. We need to ensure the framework is loaded.
|
||||
NSLog("DNP-DEBUG: Force-loading DailyNotificationPlugin framework...")
|
||||
_ = DailyNotificationPlugin.self // Force class load
|
||||
NSLog("DNP-DEBUG: DailyNotificationPlugin class reference created - framework should be loaded")
|
||||
|
||||
// Verify class is now in objc_getClassList
|
||||
let classCount = objc_getClassList(nil, 0)
|
||||
let classes = UnsafeMutablePointer<AnyClass?>.allocate(capacity: Int(classCount))
|
||||
defer { classes.deallocate() }
|
||||
let releasingClasses = AutoreleasingUnsafeMutablePointer<AnyClass>(classes)
|
||||
let numClasses = objc_getClassList(releasingClasses, Int32(classCount))
|
||||
|
||||
var foundInClassList = false
|
||||
for i in 0..<Int(numClasses) {
|
||||
if let aClass = classes[i] {
|
||||
let className = NSStringFromClass(aClass)
|
||||
if className == "DailyNotificationPlugin" {
|
||||
foundInClassList = true
|
||||
NSLog("DNP-DEBUG: ✅ DailyNotificationPlugin found in objc_getClassList")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !foundInClassList {
|
||||
NSLog("DNP-DEBUG: ❌ DailyNotificationPlugin NOT found in objc_getClassList (this is the problem!)")
|
||||
}
|
||||
|
||||
// Set notification center delegate to show notifications in foreground
|
||||
UNUserNotificationCenter.current().delegate = self
|
||||
NSLog("DNP-DEBUG: UNUserNotificationCenter delegate set to AppDelegate")
|
||||
|
||||
// Override point for customization after application launch.
|
||||
return true
|
||||
}
|
||||
@@ -27,6 +67,44 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
|
||||
|
||||
// Re-set delegate when app becomes active (in case Capacitor resets it)
|
||||
UNUserNotificationCenter.current().delegate = self
|
||||
NSLog("DNP-DEBUG: UNUserNotificationCenter delegate re-set in applicationDidBecomeActive")
|
||||
}
|
||||
|
||||
// MARK: - UNUserNotificationCenterDelegate
|
||||
|
||||
/**
|
||||
* Show notifications even when app is in foreground
|
||||
* This is required for test app to see notifications during testing
|
||||
*/
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||
NSLog("DNP-DEBUG: ✅ userNotificationCenter willPresent called!")
|
||||
NSLog("DNP-DEBUG: Notification received in foreground: %@", notification.request.identifier)
|
||||
NSLog("DNP-DEBUG: Notification title: %@", notification.request.content.title)
|
||||
NSLog("DNP-DEBUG: Notification body: %@", notification.request.content.body)
|
||||
|
||||
// Show notification with banner, sound, and badge
|
||||
// Use .banner for iOS 14+, fallback to .alert for iOS 13
|
||||
if #available(iOS 14.0, *) {
|
||||
completionHandler([.banner, .sound, .badge])
|
||||
} else {
|
||||
completionHandler([.alert, .sound, .badge])
|
||||
}
|
||||
|
||||
NSLog("DNP-DEBUG: ✅ Completion handler called with presentation options")
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle notification tap/interaction
|
||||
*/
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||
NSLog("DNP-DEBUG: Notification tapped: %@", response.notification.request.identifier)
|
||||
NSLog("DNP-DEBUG: Action identifier: %@", response.actionIdentifier)
|
||||
|
||||
// Handle notification tap if needed
|
||||
completionHandler()
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
|
||||
@@ -39,6 +39,13 @@
|
||||
<button class="action-button primary" @click="scheduleNotification" :disabled="isScheduling">
|
||||
{{ isScheduling ? 'Scheduling...' : 'Schedule Notification' }}
|
||||
</button>
|
||||
<button class="action-button secondary" @click="testNotification" :disabled="isTesting">
|
||||
{{ isTesting ? 'Testing...' : '🧪 Test Notification (5 seconds)' }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="scheduleFeedback" class="feedback-message" :class="scheduleFeedback.type">
|
||||
{{ scheduleFeedback.message }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,6 +61,8 @@ class ScheduleView extends Vue {
|
||||
notificationTitle = 'Daily Update'
|
||||
notificationMessage = 'Your daily notification is ready!'
|
||||
isScheduling = false
|
||||
isTesting = false
|
||||
scheduleFeedback: { type: 'success' | 'error' | 'info', message: string } | null = null
|
||||
|
||||
goBack() {
|
||||
router.push('/')
|
||||
@@ -61,6 +70,8 @@ class ScheduleView extends Vue {
|
||||
|
||||
async scheduleNotification() {
|
||||
this.isScheduling = true
|
||||
this.scheduleFeedback = null
|
||||
|
||||
try {
|
||||
console.log('🔄 Starting notification scheduling...')
|
||||
|
||||
@@ -70,6 +81,17 @@ class ScheduleView extends Vue {
|
||||
|
||||
console.log('✅ Plugin loaded:', plugin)
|
||||
|
||||
// Calculate when the notification will fire
|
||||
const [hours, minutes] = this.scheduleTime.split(':').map(Number)
|
||||
const now = new Date()
|
||||
const scheduledTime = new Date()
|
||||
scheduledTime.setHours(hours, minutes, 0, 0)
|
||||
|
||||
// If the time has passed today, schedule for tomorrow
|
||||
if (scheduledTime <= now) {
|
||||
scheduledTime.setDate(scheduledTime.getDate() + 1)
|
||||
}
|
||||
|
||||
const options = {
|
||||
time: this.scheduleTime,
|
||||
title: this.notificationTitle,
|
||||
@@ -84,8 +106,22 @@ class ScheduleView extends Vue {
|
||||
|
||||
console.log('✅ Notification scheduled successfully!')
|
||||
|
||||
// Show success feedback to user
|
||||
alert('Notification scheduled successfully!')
|
||||
// Show success feedback with timing info
|
||||
const timeUntil = scheduledTime.getTime() - now.getTime()
|
||||
const hoursUntil = Math.floor(timeUntil / (1000 * 60 * 60))
|
||||
const minutesUntil = Math.floor((timeUntil % (1000 * 60 * 60)) / (1000 * 60))
|
||||
|
||||
let timeMessage = ''
|
||||
if (hoursUntil > 0) {
|
||||
timeMessage = `Notification will appear in ${hoursUntil} hour${hoursUntil > 1 ? 's' : ''} and ${minutesUntil} minute${minutesUntil !== 1 ? 's' : ''} (at ${scheduledTime.toLocaleTimeString()})`
|
||||
} else {
|
||||
timeMessage = `Notification will appear in ${minutesUntil} minute${minutesUntil !== 1 ? 's' : ''} (at ${scheduledTime.toLocaleTimeString()})`
|
||||
}
|
||||
|
||||
this.scheduleFeedback = {
|
||||
type: 'success',
|
||||
message: `✅ Notification scheduled successfully! ${timeMessage}. Use "Test Notification" to see one immediately.`
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to schedule notification:', error)
|
||||
@@ -95,12 +131,59 @@ class ScheduleView extends Vue {
|
||||
stack: error.stack
|
||||
})
|
||||
|
||||
// Show error feedback to user
|
||||
alert(`Failed to schedule notification: ${error.message}`)
|
||||
this.scheduleFeedback = {
|
||||
type: 'error',
|
||||
message: `❌ Failed to schedule notification: ${error.message}`
|
||||
}
|
||||
} finally {
|
||||
this.isScheduling = false
|
||||
}
|
||||
}
|
||||
|
||||
async testNotification() {
|
||||
this.isTesting = true
|
||||
this.scheduleFeedback = null
|
||||
|
||||
try {
|
||||
console.log('🧪 Testing notification (will fire in 5 seconds)...')
|
||||
|
||||
// Import and use the real plugin
|
||||
const { DailyNotification } = await import('@timesafari/daily-notification-plugin')
|
||||
const plugin = DailyNotification
|
||||
|
||||
console.log('✅ Plugin loaded:', plugin)
|
||||
|
||||
// Use testAlarm to schedule a notification that fires in 5 seconds
|
||||
const result = await plugin.testAlarm({ secondsFromNow: 5 })
|
||||
|
||||
console.log('✅ Test notification scheduled:', result)
|
||||
|
||||
this.scheduleFeedback = {
|
||||
type: 'success',
|
||||
message: '✅ Test notification scheduled! It will appear in 5 seconds. Look for it at the top of your screen.'
|
||||
}
|
||||
|
||||
// Clear feedback after 10 seconds
|
||||
setTimeout(() => {
|
||||
this.scheduleFeedback = null
|
||||
}, 10000)
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to schedule test notification:', error)
|
||||
console.error('❌ Error details:', {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack
|
||||
})
|
||||
|
||||
this.scheduleFeedback = {
|
||||
type: 'error',
|
||||
message: `❌ Failed to schedule test notification: ${error.message}`
|
||||
}
|
||||
} finally {
|
||||
this.isTesting = false
|
||||
}
|
||||
}
|
||||
}
|
||||
export default toNative(ScheduleView)
|
||||
</script>
|
||||
@@ -229,6 +312,42 @@ export default toNative(ScheduleView)
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.action-button.secondary {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.action-button.secondary:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.feedback-message {
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.feedback-message.success {
|
||||
background: rgba(76, 175, 80, 0.2);
|
||||
border: 1px solid rgba(76, 175, 80, 0.4);
|
||||
color: #81c784;
|
||||
}
|
||||
|
||||
.feedback-message.error {
|
||||
background: rgba(244, 67, 54, 0.2);
|
||||
border: 1px solid rgba(244, 67, 54, 0.4);
|
||||
color: #e57373;
|
||||
}
|
||||
|
||||
.feedback-message.info {
|
||||
background: rgba(33, 150, 243, 0.2);
|
||||
border: 1px solid rgba(33, 150, 243, 0.4);
|
||||
color: #64b5f6;
|
||||
}
|
||||
|
||||
/* Mobile responsiveness */
|
||||
@media (max-width: 768px) {
|
||||
.schedule-view {
|
||||
|
||||
Reference in New Issue
Block a user