From edc4082f721f17c9960a4c79aa6c4295ee48b0f9 Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Wed, 31 Dec 2025 17:25:52 +0800 Subject: [PATCH] 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. --- ios/Plugin/DailyNotificationPlugin.swift | 153 ++++++++++++++++++ .../ios/App/App/AppDelegate.swift | 80 ++++++++- .../src/views/ScheduleView.vue | 127 ++++++++++++++- 3 files changed, 355 insertions(+), 5 deletions(-) diff --git a/ios/Plugin/DailyNotificationPlugin.swift b/ios/Plugin/DailyNotificationPlugin.swift index 2af6473..515d9e9 100644 --- a/ios/Plugin/DailyNotificationPlugin.swift +++ b/ios/Plugin/DailyNotificationPlugin.swift @@ -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.. 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.allocate(capacity: Int(classCount)) + defer { classes.deallocate() } + let releasingClasses = AutoreleasingUnsafeMutablePointer(classes) + let numClasses = objc_getClassList(releasingClasses, Int32(classCount)) + + var foundInClassList = false + for i in 0.. 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) { diff --git a/test-apps/daily-notification-test/src/views/ScheduleView.vue b/test-apps/daily-notification-test/src/views/ScheduleView.vue index f6b0709..3816227 100644 --- a/test-apps/daily-notification-test/src/views/ScheduleView.vue +++ b/test-apps/daily-notification-test/src/views/ScheduleView.vue @@ -39,6 +39,13 @@ + + + + @@ -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) @@ -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 {