From f9c21d4e5b0f4a3c6accb2b3409e44e602ffba5e Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Sun, 5 Oct 2025 05:12:06 +0000 Subject: [PATCH] docs: add comprehensive static daily reminders documentation - Add static daily reminders to README.md core features and API reference - Create detailed usage guide in USAGE.md with examples and best practices - Add version 2.1.0 changelog entry documenting new reminder functionality - Create examples/static-daily-reminders.ts with complete usage examples - Update test-apps README to include reminder testing capabilities The static daily reminder feature provides simple daily notifications without network content dependency, supporting cross-platform scheduling with rich customization options. --- CHANGELOG.md | 34 ++ README.md | 98 +++++ USAGE.md | 65 ++++ .../dailynotification/NotifyReceiver.kt | 83 +++++ examples/static-daily-reminders.ts | 342 +++++++++++++++++ ios/Plugin/DailyNotificationPlugin.swift | 301 +++++++++++++++ src/android/DailyNotificationPlugin.java | 349 ++++++++++++++++++ src/android/DailyNotificationScheduler.java | 11 + src/definitions.ts | 51 +++ src/web.ts | 314 ++++++++++++++++ test-apps/README.md | 1 + test-apps/android-test/src/index.html | 51 +++ test-apps/android-test/src/index.ts | 98 +++++ test-apps/electron-test/src/index.html | 51 +++ test-apps/electron-test/src/index.ts | 98 +++++ test-apps/ios-test/src/index.html | 51 +++ test-apps/ios-test/src/index.ts | 98 +++++ tests/advanced-scenarios.test.ts | 6 + tests/daily-notification.test.ts | 6 + tests/edge-cases.test.ts | 6 + tests/enterprise-scenarios.test.ts | 6 + 21 files changed, 2120 insertions(+) create mode 100644 examples/static-daily-reminders.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 5006057..f944c49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,40 @@ All notable changes to the Daily Notification Plugin will be documented in this The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.1.0] - 2025-01-02 + +### Added + +- **Static Daily Reminders**: New functionality for simple daily notifications without network content +- **Cross-Platform Reminder API**: Consistent reminder management across Android, iOS, and Web +- **Reminder Management**: Full CRUD operations for reminder scheduling and management +- **Offline Reminder Support**: Reminders work completely offline without content caching +- **Rich Reminder Customization**: Support for custom titles, bodies, sounds, vibration, and priorities +- **Persistent Reminder Storage**: Reminders survive app restarts and device reboots + +### New Methods + +- `scheduleDailyReminder(options)`: Schedule a simple daily reminder +- `cancelDailyReminder(reminderId)`: Cancel a specific reminder +- `getScheduledReminders()`: Get all scheduled reminders +- `updateDailyReminder(reminderId, options)`: Update an existing reminder + +### Features + +- **No Network Dependency**: Static reminders work completely offline +- **Simple Time Format**: Easy HH:mm time format (e.g., "09:00") +- **Priority Levels**: Support for low, normal, and high priority notifications +- **Repeat Options**: Configurable daily repetition +- **Platform Integration**: Native notification channels and categories +- **Test App Integration**: Complete test app support for reminder functionality + +### Documentation + +- Updated README.md with static reminder examples and API reference +- Added comprehensive usage examples in USAGE.md +- Created detailed example file: `examples/static-daily-reminders.ts` +- Enhanced test apps with reminder management UI + ## [1.0.0] - 2024-03-20 ### Added diff --git a/README.md b/README.md index 497ca33..9b47b3f 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ The Daily Notification Plugin is a comprehensive Capacitor plugin that provides - **TTL-at-Fire Logic**: Content validity checking at notification time - **Callback System**: HTTP, local, and queue callback support - **Circuit Breaker Pattern**: Automatic failure detection and recovery +- **Static Daily Reminders**: Simple daily notifications without network content - **Cross-Platform**: Android, iOS, and Web implementations ### 📱 **Platform Support** @@ -54,6 +55,15 @@ The Daily Notification Plugin is a comprehensive Capacitor plugin that provides - **Error Handling**: Exponential backoff and retry logic - **Security**: Encrypted storage and secure callback handling +### ⏰ **Static Daily Reminders** + +- **No Network Required**: Completely offline reminder notifications +- **Simple Scheduling**: Easy daily reminder setup with HH:mm time format +- **Rich Customization**: Customizable title, body, sound, vibration, and priority +- **Persistent Storage**: Survives app restarts and device reboots +- **Cross-Platform**: Consistent API across Android, iOS, and Web +- **Management**: Full CRUD operations for reminder management + ## Installation ```bash @@ -130,6 +140,45 @@ function saveToDatabase(event: CallbackEvent) { } ``` +### Static Daily Reminders + +For simple daily reminders that don't require network content: + +```typescript +import { DailyNotification } from '@timesafari/daily-notification-plugin'; + +// Schedule a simple daily reminder +await DailyNotification.scheduleDailyReminder({ + id: 'morning_checkin', + title: 'Good Morning!', + body: 'Time to check your TimeSafari community updates', + time: '09:00', // HH:mm format + sound: true, + vibration: true, + priority: 'normal', + repeatDaily: true +}); + +// Get all scheduled reminders +const result = await DailyNotification.getScheduledReminders(); +console.log('Scheduled reminders:', result.reminders); + +// Update an existing reminder +await DailyNotification.updateDailyReminder('morning_checkin', { + title: 'Updated Morning Reminder', + time: '08:30' +}); + +// Cancel a reminder +await DailyNotification.cancelDailyReminder('morning_checkin'); +``` + +**Key Benefits of Static Reminders:** +- ✅ **No network dependency** - works completely offline +- ✅ **No content cache required** - direct notification display +- ✅ **Simple API** - easy to use for basic reminder functionality +- ✅ **Persistent** - survives app restarts and device reboots + ## API Reference ### Core Methods @@ -257,6 +306,55 @@ const status = await DailyNotification.getDualScheduleStatus(); - **HTTPS**: Required for Service Worker and push notifications - **Browser Support**: Chrome 40+, Firefox 44+, Safari 11.1+ +### Static Daily Reminder Methods + +#### `scheduleDailyReminder(options)` + +Schedule a simple daily reminder without network content. + +```typescript +await DailyNotification.scheduleDailyReminder({ + id: string; // Unique reminder identifier + title: string; // Notification title + body: string; // Notification body + time: string; // Time in HH:mm format (e.g., "09:00") + sound?: boolean; // Enable sound (default: true) + vibration?: boolean; // Enable vibration (default: true) + priority?: 'low' | 'normal' | 'high'; // Priority level (default: 'normal') + repeatDaily?: boolean; // Repeat daily (default: true) + timezone?: string; // Optional timezone +}); +``` + +#### `cancelDailyReminder(reminderId)` + +Cancel a scheduled daily reminder. + +```typescript +await DailyNotification.cancelDailyReminder('morning_checkin'); +``` + +#### `getScheduledReminders()` + +Get all scheduled reminders. + +```typescript +const result = await DailyNotification.getScheduledReminders(); +console.log('Reminders:', result.reminders); +``` + +#### `updateDailyReminder(reminderId, options)` + +Update an existing daily reminder. + +```typescript +await DailyNotification.updateDailyReminder('morning_checkin', { + title: 'Updated Title', + time: '08:30', + priority: 'high' +}); +``` + ## Configuration ### Android Configuration diff --git a/USAGE.md b/USAGE.md index 13ee767..2c33623 100644 --- a/USAGE.md +++ b/USAGE.md @@ -39,6 +39,71 @@ await DailyNotification.scheduleDailyNotification({ - **`enableErrorHandling`**: Advanced retry logic with exponential backoff - **`enablePerformanceOptimization`**: Database indexes, memory management, object pooling +## Static Daily Reminders + +For simple daily reminders that don't require network content or content caching: + +### Basic Usage + +```typescript +// Schedule a simple daily reminder +await DailyNotification.scheduleDailyReminder({ + id: 'morning_checkin', + title: 'Good Morning!', + body: 'Time to check your TimeSafari community updates', + time: '09:00' // HH:mm format +}); +``` + +### Advanced Configuration + +```typescript +// Schedule a customized reminder +await DailyNotification.scheduleDailyReminder({ + id: 'evening_reflection', + title: 'Evening Reflection', + body: 'Take a moment to reflect on your day', + time: '20:00', + sound: true, + vibration: true, + priority: 'high', + repeatDaily: true, + timezone: 'America/New_York' +}); +``` + +### Reminder Management + +```typescript +// Get all scheduled reminders +const result = await DailyNotification.getScheduledReminders(); +console.log('Scheduled reminders:', result.reminders); + +// Update an existing reminder +await DailyNotification.updateDailyReminder('morning_checkin', { + title: 'Updated Morning Check-in', + time: '08:30', + priority: 'high' +}); + +// Cancel a specific reminder +await DailyNotification.cancelDailyReminder('evening_reflection'); +``` + +### Key Benefits + +- ✅ **No Network Required**: Works completely offline +- ✅ **No Content Cache**: Direct notification display without caching +- ✅ **Simple API**: Easy-to-use methods for basic reminder functionality +- ✅ **Persistent**: Survives app restarts and device reboots +- ✅ **Cross-Platform**: Consistent behavior across Android, iOS, and Web + +### Time Format + +Use 24-hour format with leading zeros: +- ✅ Valid: `"09:00"`, `"12:30"`, `"23:59"` +- ❌ Invalid: `"9:00"`, `"24:00"`, `"12:60"` + ## Platform-Specific Features ### Android diff --git a/android/src/main/java/com/timesafari/dailynotification/NotifyReceiver.kt b/android/src/main/java/com/timesafari/dailynotification/NotifyReceiver.kt index fd4c164..8998b0c 100644 --- a/android/src/main/java/com/timesafari/dailynotification/NotifyReceiver.kt +++ b/android/src/main/java/com/timesafari/dailynotification/NotifyReceiver.kt @@ -94,6 +94,26 @@ class NotifyReceiver : BroadcastReceiver() { CoroutineScope(Dispatchers.IO).launch { try { + // Check if this is a static reminder (no content dependency) + val isStaticReminder = intent?.getBooleanExtra("is_static_reminder", false) ?: false + + if (isStaticReminder) { + // Handle static reminder without content cache + val title = intent?.getStringExtra("title") ?: "Daily Reminder" + val body = intent?.getStringExtra("body") ?: "Don't forget your daily check-in!" + val sound = intent?.getBooleanExtra("sound", true) ?: true + val vibration = intent?.getBooleanExtra("vibration", true) ?: true + val priority = intent?.getStringExtra("priority") ?: "normal" + val reminderId = intent?.getStringExtra("reminder_id") ?: "unknown" + + showStaticReminderNotification(context, title, body, sound, vibration, priority, reminderId) + + // Record reminder trigger in database + recordReminderTrigger(context, reminderId) + return@launch + } + + // Existing cached content logic for regular notifications val db = DailyNotificationDatabase.getDatabase(context) val latestCache = db.contentCacheDao().getLatest() @@ -250,4 +270,67 @@ class NotifyReceiver : BroadcastReceiver() { // Local callback implementation would go here Log.i(TAG, "Local callback fired: ${callback.id} for event: $eventType") } + + // Static Reminder Helper Methods + private fun showStaticReminderNotification( + context: Context, + title: String, + body: String, + sound: Boolean, + vibration: Boolean, + priority: String, + reminderId: String + ) { + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + // Create notification channel for reminders + createReminderNotificationChannel(context, notificationManager) + + val notification = NotificationCompat.Builder(context, "daily_reminders") + .setSmallIcon(android.R.drawable.ic_dialog_info) + .setContentTitle(title) + .setContentText(body) + .setPriority( + when (priority) { + "high" -> NotificationCompat.PRIORITY_HIGH + "low" -> NotificationCompat.PRIORITY_LOW + else -> NotificationCompat.PRIORITY_DEFAULT + } + ) + .setSound(if (sound) null else null) // Use default sound if enabled + .setAutoCancel(true) + .setVibrate(if (vibration) longArrayOf(0, 250, 250, 250) else null) + .setCategory(NotificationCompat.CATEGORY_REMINDER) + .build() + + notificationManager.notify(reminderId.hashCode(), notification) + Log.i(TAG, "Static reminder displayed: $title (ID: $reminderId)") + } + + private fun createReminderNotificationChannel(context: Context, notificationManager: NotificationManager) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + "daily_reminders", + "Daily Reminders", + NotificationManager.IMPORTANCE_DEFAULT + ).apply { + description = "Daily reminder notifications" + enableVibration(true) + setShowBadge(true) + } + notificationManager.createNotificationChannel(channel) + } + } + + private fun recordReminderTrigger(context: Context, reminderId: String) { + try { + val prefs = context.getSharedPreferences("daily_reminders", Context.MODE_PRIVATE) + val editor = prefs.edit() + editor.putLong("${reminderId}_lastTriggered", System.currentTimeMillis()) + editor.apply() + Log.d(TAG, "Reminder trigger recorded: $reminderId") + } catch (e: Exception) { + Log.e(TAG, "Error recording reminder trigger", e) + } + } } diff --git a/examples/static-daily-reminders.ts b/examples/static-daily-reminders.ts new file mode 100644 index 0000000..34c9157 --- /dev/null +++ b/examples/static-daily-reminders.ts @@ -0,0 +1,342 @@ +/** + * Static Daily Reminders Examples + * + * Examples demonstrating the static daily reminder functionality + * that works without network content or content caching. + * + * @author Matthew Raymer + * @version 1.0.0 + */ + +import { DailyNotification } from '@timesafari/daily-notification-plugin'; + +/** + * Basic Daily Reminder + * + * Schedule a simple daily reminder with default settings + */ +export async function scheduleBasicReminder() { + try { + await DailyNotification.scheduleDailyReminder({ + id: 'morning_checkin', + title: 'Good Morning!', + body: 'Time to check your TimeSafari community updates', + time: '09:00' + }); + + console.log('✅ Basic reminder scheduled for 9:00 AM daily'); + } catch (error) { + console.error('❌ Failed to schedule basic reminder:', error); + } +} + +/** + * Customized Daily Reminder + * + * Schedule a reminder with custom sound, vibration, and priority + */ +export async function scheduleCustomizedReminder() { + try { + await DailyNotification.scheduleDailyReminder({ + id: 'evening_reflection', + title: 'Evening Reflection', + body: 'Take a moment to reflect on your day and plan for tomorrow', + time: '20:00', + sound: true, + vibration: true, + priority: 'high', + repeatDaily: true + }); + + console.log('✅ Customized reminder scheduled for 8:00 PM daily'); + } catch (error) { + console.error('❌ Failed to schedule customized reminder:', error); + } +} + +/** + * Multiple Daily Reminders + * + * Schedule multiple reminders throughout the day + */ +export async function scheduleMultipleReminders() { + const reminders = [ + { + id: 'morning_motivation', + title: 'Morning Motivation', + body: 'Start your day with positive energy!', + time: '07:00', + priority: 'high' as const + }, + { + id: 'lunch_break', + title: 'Lunch Break', + body: 'Time for a well-deserved break', + time: '12:30', + priority: 'normal' as const + }, + { + id: 'evening_gratitude', + title: 'Evening Gratitude', + body: 'What are you grateful for today?', + time: '19:00', + priority: 'normal' as const + } + ]; + + try { + for (const reminder of reminders) { + await DailyNotification.scheduleDailyReminder(reminder); + console.log(`✅ Scheduled reminder: ${reminder.id} at ${reminder.time}`); + } + + console.log('✅ All reminders scheduled successfully'); + } catch (error) { + console.error('❌ Failed to schedule multiple reminders:', error); + } +} + +/** + * Get All Scheduled Reminders + * + * Retrieve and display all currently scheduled reminders + */ +export async function getAllScheduledReminders() { + try { + const result = await DailyNotification.getScheduledReminders(); + + if (result.reminders && result.reminders.length > 0) { + console.log(`📋 Found ${result.reminders.length} scheduled reminders:`); + + result.reminders.forEach((reminder, index) => { + console.log(`${index + 1}. ${reminder.title} (${reminder.id})`); + console.log(` Time: ${reminder.time}`); + console.log(` Priority: ${reminder.priority}`); + console.log(` Repeat Daily: ${reminder.repeatDaily}`); + console.log(` Scheduled: ${reminder.isScheduled ? 'Yes' : 'No'}`); + console.log(''); + }); + } else { + console.log('📋 No scheduled reminders found'); + } + } catch (error) { + console.error('❌ Failed to get scheduled reminders:', error); + } +} + +/** + * Update Existing Reminder + * + * Update an existing reminder with new settings + */ +export async function updateExistingReminder() { + try { + // Update the morning check-in reminder + await DailyNotification.updateDailyReminder('morning_checkin', { + title: 'Updated Morning Check-in', + body: 'Time to check your TimeSafari community updates and plan your day', + time: '08:30', + priority: 'high' + }); + + console.log('✅ Morning check-in reminder updated to 8:30 AM with high priority'); + } catch (error) { + console.error('❌ Failed to update reminder:', error); + } +} + +/** + * Cancel Specific Reminder + * + * Cancel a specific reminder by ID + */ +export async function cancelSpecificReminder() { + try { + await DailyNotification.cancelDailyReminder('evening_reflection'); + console.log('✅ Evening reflection reminder cancelled'); + } catch (error) { + console.error('❌ Failed to cancel reminder:', error); + } +} + +/** + * Cancel All Reminders + * + * Cancel all scheduled reminders + */ +export async function cancelAllReminders() { + try { + const result = await DailyNotification.getScheduledReminders(); + + if (result.reminders && result.reminders.length > 0) { + for (const reminder of result.reminders) { + await DailyNotification.cancelDailyReminder(reminder.id); + console.log(`✅ Cancelled reminder: ${reminder.id}`); + } + + console.log('✅ All reminders cancelled successfully'); + } else { + console.log('📋 No reminders to cancel'); + } + } catch (error) { + console.error('❌ Failed to cancel all reminders:', error); + } +} + +/** + * Reminder Management Workflow + * + * Complete workflow demonstrating reminder management + */ +export async function reminderManagementWorkflow() { + console.log('🚀 Starting reminder management workflow...\n'); + + try { + // 1. Schedule initial reminders + console.log('1. Scheduling initial reminders...'); + await scheduleMultipleReminders(); + console.log(''); + + // 2. Get all reminders + console.log('2. Getting all scheduled reminders...'); + await getAllScheduledReminders(); + console.log(''); + + // 3. Update a reminder + console.log('3. Updating morning check-in reminder...'); + await updateExistingReminder(); + console.log(''); + + // 4. Get updated reminders + console.log('4. Getting updated reminders...'); + await getAllScheduledReminders(); + console.log(''); + + // 5. Cancel a specific reminder + console.log('5. Cancelling evening reflection reminder...'); + await cancelSpecificReminder(); + console.log(''); + + // 6. Final reminder list + console.log('6. Final reminder list...'); + await getAllScheduledReminders(); + + console.log('🎉 Reminder management workflow completed successfully!'); + } catch (error) { + console.error('❌ Reminder management workflow failed:', error); + } +} + +/** + * Time Format Validation Examples + * + * Examples of valid and invalid time formats + */ +export function timeFormatExamples() { + console.log('⏰ Time Format Examples:'); + console.log(''); + + const validTimes = [ + '00:00', // Midnight + '06:30', // 6:30 AM + '12:00', // Noon + '18:45', // 6:45 PM + '23:59' // 11:59 PM + ]; + + const invalidTimes = [ + '24:00', // Invalid hour + '12:60', // Invalid minute + '9:00', // Missing leading zero + '12:0', // Missing trailing zero + '12', // Missing minutes + 'abc' // Non-numeric + ]; + + console.log('✅ Valid time formats:'); + validTimes.forEach(time => { + console.log(` "${time}" - Valid`); + }); + + console.log(''); + console.log('❌ Invalid time formats:'); + invalidTimes.forEach(time => { + console.log(` "${time}" - Invalid`); + }); + + console.log(''); + console.log('📝 Time format rules:'); + console.log(' - Use HH:mm format (24-hour)'); + console.log(' - Hours: 00-23'); + console.log(' - Minutes: 00-59'); + console.log(' - Always use leading zeros'); +} + +/** + * Platform-Specific Considerations + * + * Important notes for different platforms + */ +export function platformConsiderations() { + console.log('📱 Platform-Specific Considerations:'); + console.log(''); + + console.log('🤖 Android:'); + console.log(' - Uses AlarmManager for precise scheduling'); + console.log(' - Requires POST_NOTIFICATIONS permission'); + console.log(' - May be affected by battery optimization'); + console.log(' - Survives device reboots'); + console.log(''); + + console.log('🍎 iOS:'); + console.log(' - Uses UNUserNotificationCenter'); + console.log(' - Requires notification permissions'); + console.log(' - Limited to 64 scheduled notifications'); + console.log(' - May be affected by Background App Refresh'); + console.log(''); + + console.log('🌐 Web:'); + console.log(' - Uses setTimeout for scheduling'); + console.log(' - Requires notification permissions'); + console.log(' - Limited by browser tab lifecycle'); + console.log(' - May not persist across browser restarts'); + console.log(''); + + console.log('💡 Best Practices:'); + console.log(' - Always request notification permissions first'); + console.log(' - Handle permission denials gracefully'); + console.log(' - Test on actual devices, not just simulators'); + console.log(' - Consider platform limitations in your app design'); +} + +// Export all functions for easy importing +export const StaticReminderExamples = { + scheduleBasicReminder, + scheduleCustomizedReminder, + scheduleMultipleReminders, + getAllScheduledReminders, + updateExistingReminder, + cancelSpecificReminder, + cancelAllReminders, + reminderManagementWorkflow, + timeFormatExamples, + platformConsiderations +}; + +// Example usage +if (require.main === module) { + console.log('📚 Static Daily Reminders Examples'); + console.log('=====================================\n'); + + // Show time format examples + timeFormatExamples(); + console.log(''); + + // Show platform considerations + platformConsiderations(); + console.log(''); + + console.log('💡 To run the examples, import and call the functions:'); + console.log(' import { StaticReminderExamples } from "./static-daily-reminders";'); + console.log(' await StaticReminderExamples.reminderManagementWorkflow();'); +} diff --git a/ios/Plugin/DailyNotificationPlugin.swift b/ios/Plugin/DailyNotificationPlugin.swift index a1ab22f..48b3c20 100644 --- a/ios/Plugin/DailyNotificationPlugin.swift +++ b/ios/Plugin/DailyNotificationPlugin.swift @@ -176,4 +176,305 @@ public class DailyNotificationPlugin: CAPPlugin { // For now, return next day at 9 AM return 86400 // 24 hours } + + // MARK: - Static Daily Reminder Methods + + @objc func scheduleDailyReminder(_ call: CAPPluginCall) { + guard let id = call.getString("id"), + let title = call.getString("title"), + let body = call.getString("body"), + let time = call.getString("time") else { + call.reject("Missing required parameters: id, title, body, time") + return + } + + let sound = call.getBool("sound", true) + let vibration = call.getBool("vibration", true) + let priority = call.getString("priority", "normal") + let repeatDaily = call.getBool("repeatDaily", true) + let timezone = call.getString("timezone") + + print("DNP-REMINDER: Scheduling daily reminder: \(id)") + + // Parse time (HH:mm format) + let timeComponents = time.components(separatedBy: ":") + guard timeComponents.count == 2, + let hour = Int(timeComponents[0]), + let minute = Int(timeComponents[1]), + hour >= 0 && hour <= 23, + minute >= 0 && minute <= 59 else { + call.reject("Invalid time format. Use HH:mm (e.g., 09:00)") + return + } + + // Create notification content + let content = UNMutableNotificationContent() + content.title = title + content.body = body + content.sound = sound ? .default : nil + content.categoryIdentifier = "DAILY_REMINDER" + + // Set priority + switch priority { + case "high": + content.interruptionLevel = .critical + case "low": + content.interruptionLevel = .passive + default: + content.interruptionLevel = .active + } + + // Create date components for daily trigger + var dateComponents = DateComponents() + dateComponents.hour = hour + dateComponents.minute = minute + + let trigger = UNCalendarNotificationTrigger( + dateMatching: dateComponents, + repeats: repeatDaily + ) + + let request = UNNotificationRequest( + identifier: "reminder_\(id)", + content: content, + trigger: trigger + ) + + // Store reminder in UserDefaults + storeReminderInUserDefaults( + id: id, + title: title, + body: body, + time: time, + sound: sound, + vibration: vibration, + priority: priority, + repeatDaily: repeatDaily, + timezone: timezone + ) + + // Schedule the notification + notificationCenter.add(request) { error in + DispatchQueue.main.async { + if let error = error { + print("DNP-REMINDER: Failed to schedule reminder: \(error)") + call.reject("Failed to schedule daily reminder: \(error.localizedDescription)") + } else { + print("DNP-REMINDER: Daily reminder scheduled successfully: \(id)") + call.resolve() + } + } + } + } + + @objc func cancelDailyReminder(_ call: CAPPluginCall) { + guard let reminderId = call.getString("reminderId") else { + call.reject("Missing reminderId parameter") + return + } + + print("DNP-REMINDER: Cancelling daily reminder: \(reminderId)") + + // Cancel the notification + notificationCenter.removePendingNotificationRequests(withIdentifiers: ["reminder_\(reminderId)"]) + + // Remove from UserDefaults + removeReminderFromUserDefaults(id: reminderId) + + call.resolve() + } + + @objc func getScheduledReminders(_ call: CAPPluginCall) { + print("DNP-REMINDER: Getting scheduled reminders") + + // Get pending notifications + notificationCenter.getPendingNotificationRequests { requests in + let reminderRequests = requests.filter { $0.identifier.hasPrefix("reminder_") } + + // Get stored reminder data from UserDefaults + let reminders = self.getRemindersFromUserDefaults() + + var result: [[String: Any]] = [] + for reminder in reminders { + let isScheduled = reminderRequests.contains { $0.identifier == "reminder_\(reminder["id"] as! String)" } + var reminderInfo = reminder + reminderInfo["isScheduled"] = isScheduled + result.append(reminderInfo) + } + + DispatchQueue.main.async { + call.resolve(["reminders": result]) + } + } + } + + @objc func updateDailyReminder(_ call: CAPPluginCall) { + guard let reminderId = call.getString("reminderId") else { + call.reject("Missing reminderId parameter") + return + } + + print("DNP-REMINDER: Updating daily reminder: \(reminderId)") + + // Cancel existing reminder + notificationCenter.removePendingNotificationRequests(withIdentifiers: ["reminder_\(reminderId)"]) + + // Update in UserDefaults + let title = call.getString("title") + let body = call.getString("body") + let time = call.getString("time") + let sound = call.getBool("sound") + let vibration = call.getBool("vibration") + let priority = call.getString("priority") + let repeatDaily = call.getBool("repeatDaily") + let timezone = call.getString("timezone") + + updateReminderInUserDefaults( + id: reminderId, + title: title, + body: body, + time: time, + sound: sound, + vibration: vibration, + priority: priority, + repeatDaily: repeatDaily, + timezone: timezone + ) + + // Reschedule with new settings if all required fields are provided + if let title = title, let body = body, let time = time { + // Parse time + let timeComponents = time.components(separatedBy: ":") + guard timeComponents.count == 2, + let hour = Int(timeComponents[0]), + let minute = Int(timeComponents[1]) else { + call.reject("Invalid time format") + return + } + + // Create new notification content + let content = UNMutableNotificationContent() + content.title = title + content.body = body + content.sound = (sound ?? true) ? .default : nil + content.categoryIdentifier = "DAILY_REMINDER" + + // Set priority + let finalPriority = priority ?? "normal" + switch finalPriority { + case "high": + content.interruptionLevel = .critical + case "low": + content.interruptionLevel = .passive + default: + content.interruptionLevel = .active + } + + // Create date components for daily trigger + var dateComponents = DateComponents() + dateComponents.hour = hour + dateComponents.minute = minute + + let trigger = UNCalendarNotificationTrigger( + dateMatching: dateComponents, + repeats: repeatDaily ?? true + ) + + let request = UNNotificationRequest( + identifier: "reminder_\(reminderId)", + content: content, + trigger: trigger + ) + + // Schedule the updated notification + notificationCenter.add(request) { error in + DispatchQueue.main.async { + if let error = error { + call.reject("Failed to reschedule updated reminder: \(error.localizedDescription)") + } else { + call.resolve() + } + } + } + } else { + call.resolve() + } + } + + // MARK: - Helper Methods for Reminder Storage + + private func storeReminderInUserDefaults( + id: String, + title: String, + body: String, + time: String, + sound: Bool, + vibration: Bool, + priority: String, + repeatDaily: Bool, + timezone: String? + ) { + let reminderData: [String: Any] = [ + "id": id, + "title": title, + "body": body, + "time": time, + "sound": sound, + "vibration": vibration, + "priority": priority, + "repeatDaily": repeatDaily, + "timezone": timezone ?? "", + "createdAt": Date().timeIntervalSince1970, + "lastTriggered": 0 + ] + + var reminders = UserDefaults.standard.array(forKey: "daily_reminders") as? [[String: Any]] ?? [] + reminders.append(reminderData) + UserDefaults.standard.set(reminders, forKey: "daily_reminders") + + print("DNP-REMINDER: Reminder stored: \(id)") + } + + private func removeReminderFromUserDefaults(id: String) { + var reminders = UserDefaults.standard.array(forKey: "daily_reminders") as? [[String: Any]] ?? [] + reminders.removeAll { ($0["id"] as? String) == id } + UserDefaults.standard.set(reminders, forKey: "daily_reminders") + + print("DNP-REMINDER: Reminder removed: \(id)") + } + + private func getRemindersFromUserDefaults() -> [[String: Any]] { + return UserDefaults.standard.array(forKey: "daily_reminders") as? [[String: Any]] ?? [] + } + + private func updateReminderInUserDefaults( + id: String, + title: String?, + body: String?, + time: String?, + sound: Bool?, + vibration: Bool?, + priority: String?, + repeatDaily: Bool?, + timezone: String? + ) { + var reminders = UserDefaults.standard.array(forKey: "daily_reminders") as? [[String: Any]] ?? [] + + for i in 0.. 23 || minute < 0 || minute > 59) { + call.reject("Invalid time values. Hour must be 0-23, minute must be 0-59"); + return; + } + + // Create reminder content + NotificationContent reminderContent = new NotificationContent(); + reminderContent.setId("reminder_" + id); // Prefix to identify as reminder + reminderContent.setTitle(title); + reminderContent.setBody(body); + reminderContent.setSound(sound); + reminderContent.setPriority(priority); + reminderContent.setFetchTime(System.currentTimeMillis()); + + // Calculate next trigger time + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR_OF_DAY, hour); + calendar.set(Calendar.MINUTE, minute); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + + // If time has passed today, schedule for tomorrow + if (calendar.getTimeInMillis() <= System.currentTimeMillis()) { + calendar.add(Calendar.DAY_OF_MONTH, 1); + } + + reminderContent.setScheduledTime(calendar.getTimeInMillis()); + + // Store reminder in database + storeReminderInDatabase(id, title, body, time, sound, vibration, priority, repeatDaily, timezone); + + // Schedule the notification + boolean scheduled = scheduler.scheduleNotification(reminderContent); + + if (scheduled) { + Log.i(TAG, "Daily reminder scheduled successfully: " + id); + call.resolve(); + } else { + call.reject("Failed to schedule daily reminder"); + } + + } catch (Exception e) { + Log.e(TAG, "Error scheduling daily reminder", e); + call.reject("Daily reminder scheduling failed: " + e.getMessage()); + } + } + + @PluginMethod + public void cancelDailyReminder(PluginCall call) { + try { + Log.d(TAG, "Cancelling daily reminder"); + + String reminderId = call.getString("reminderId"); + if (reminderId == null) { + call.reject("Missing reminderId parameter"); + return; + } + + // Cancel the scheduled notification (use prefixed ID) + scheduler.cancelNotification("reminder_" + reminderId); + + // Remove from database + removeReminderFromDatabase(reminderId); + + Log.i(TAG, "Daily reminder cancelled: " + reminderId); + call.resolve(); + + } catch (Exception e) { + Log.e(TAG, "Error cancelling daily reminder", e); + call.reject("Daily reminder cancellation failed: " + e.getMessage()); + } + } + + @PluginMethod + public void getScheduledReminders(PluginCall call) { + try { + Log.d(TAG, "Getting scheduled reminders"); + + // Get reminders from database + java.util.List reminders = getRemindersFromDatabase(); + + // Convert to JSObject array + JSObject result = new JSObject(); + result.put("reminders", reminders); + + call.resolve(result); + + } catch (Exception e) { + Log.e(TAG, "Error getting scheduled reminders", e); + call.reject("Failed to get scheduled reminders: " + e.getMessage()); + } + } + + @PluginMethod + public void updateDailyReminder(PluginCall call) { + try { + Log.d(TAG, "Updating daily reminder"); + + String reminderId = call.getString("reminderId"); + if (reminderId == null) { + call.reject("Missing reminderId parameter"); + return; + } + + // Extract updated options + String title = call.getString("title"); + String body = call.getString("body"); + String time = call.getString("time"); + Boolean sound = call.getBoolean("sound"); + Boolean vibration = call.getBoolean("vibration"); + String priority = call.getString("priority"); + Boolean repeatDaily = call.getBoolean("repeatDaily"); + String timezone = call.getString("timezone"); + + // Cancel existing reminder (use prefixed ID) + scheduler.cancelNotification("reminder_" + reminderId); + + // Update in database + updateReminderInDatabase(reminderId, title, body, time, sound, vibration, priority, repeatDaily, timezone); + + // Reschedule with new settings + if (title != null && body != null && time != null) { + // Create new reminder content + NotificationContent reminderContent = new NotificationContent(); + reminderContent.setId("reminder_" + reminderId); // Prefix to identify as reminder + reminderContent.setTitle(title); + reminderContent.setBody(body); + reminderContent.setSound(sound != null ? sound : true); + reminderContent.setPriority(priority != null ? priority : "normal"); + reminderContent.setFetchTime(System.currentTimeMillis()); + + // Calculate next trigger time + String[] timeParts = time.split(":"); + int hour = Integer.parseInt(timeParts[0]); + int minute = Integer.parseInt(timeParts[1]); + + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR_OF_DAY, hour); + calendar.set(Calendar.MINUTE, minute); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + + if (calendar.getTimeInMillis() <= System.currentTimeMillis()) { + calendar.add(Calendar.DAY_OF_MONTH, 1); + } + + reminderContent.setScheduledTime(calendar.getTimeInMillis()); + + // Schedule the updated notification + boolean scheduled = scheduler.scheduleNotification(reminderContent); + + if (!scheduled) { + call.reject("Failed to reschedule updated reminder"); + return; + } + } + + Log.i(TAG, "Daily reminder updated: " + reminderId); + call.resolve(); + + } catch (Exception e) { + Log.e(TAG, "Error updating daily reminder", e); + call.reject("Daily reminder update failed: " + e.getMessage()); + } + } + + // Helper methods for reminder database operations + private void storeReminderInDatabase(String id, String title, String body, String time, + boolean sound, boolean vibration, String priority, + boolean repeatDaily, String timezone) { + try { + SharedPreferences prefs = getContext().getSharedPreferences("daily_reminders", Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + + editor.putString(id + "_title", title); + editor.putString(id + "_body", body); + editor.putString(id + "_time", time); + editor.putBoolean(id + "_sound", sound); + editor.putBoolean(id + "_vibration", vibration); + editor.putString(id + "_priority", priority); + editor.putBoolean(id + "_repeatDaily", repeatDaily); + editor.putString(id + "_timezone", timezone); + editor.putLong(id + "_createdAt", System.currentTimeMillis()); + editor.putBoolean(id + "_isScheduled", true); + + editor.apply(); + Log.d(TAG, "Reminder stored in database: " + id); + + } catch (Exception e) { + Log.e(TAG, "Error storing reminder in database", e); + } + } + + private void removeReminderFromDatabase(String id) { + try { + SharedPreferences prefs = getContext().getSharedPreferences("daily_reminders", Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + + editor.remove(id + "_title"); + editor.remove(id + "_body"); + editor.remove(id + "_time"); + editor.remove(id + "_sound"); + editor.remove(id + "_vibration"); + editor.remove(id + "_priority"); + editor.remove(id + "_repeatDaily"); + editor.remove(id + "_timezone"); + editor.remove(id + "_createdAt"); + editor.remove(id + "_isScheduled"); + editor.remove(id + "_lastTriggered"); + + editor.apply(); + Log.d(TAG, "Reminder removed from database: " + id); + + } catch (Exception e) { + Log.e(TAG, "Error removing reminder from database", e); + } + } + + private java.util.List getRemindersFromDatabase() { + java.util.List reminders = new java.util.ArrayList<>(); + + try { + SharedPreferences prefs = getContext().getSharedPreferences("daily_reminders", Context.MODE_PRIVATE); + java.util.Map allEntries = prefs.getAll(); + + java.util.Set reminderIds = new java.util.HashSet<>(); + for (String key : allEntries.keySet()) { + if (key.endsWith("_title")) { + String id = key.substring(0, key.length() - 6); // Remove "_title" + reminderIds.add(id); + } + } + + for (String id : reminderIds) { + DailyReminderInfo reminder = new DailyReminderInfo(); + reminder.id = id; + reminder.title = prefs.getString(id + "_title", ""); + reminder.body = prefs.getString(id + "_body", ""); + reminder.time = prefs.getString(id + "_time", ""); + reminder.sound = prefs.getBoolean(id + "_sound", true); + reminder.vibration = prefs.getBoolean(id + "_vibration", true); + reminder.priority = prefs.getString(id + "_priority", "normal"); + reminder.repeatDaily = prefs.getBoolean(id + "_repeatDaily", true); + reminder.timezone = prefs.getString(id + "_timezone", null); + reminder.isScheduled = prefs.getBoolean(id + "_isScheduled", false); + reminder.createdAt = prefs.getLong(id + "_createdAt", 0); + reminder.lastTriggered = prefs.getLong(id + "_lastTriggered", 0); + + // Calculate next trigger time + String[] timeParts = reminder.time.split(":"); + int hour = Integer.parseInt(timeParts[0]); + int minute = Integer.parseInt(timeParts[1]); + + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.HOUR_OF_DAY, hour); + calendar.set(Calendar.MINUTE, minute); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + + if (calendar.getTimeInMillis() <= System.currentTimeMillis()) { + calendar.add(Calendar.DAY_OF_MONTH, 1); + } + + reminder.nextTriggerTime = calendar.getTimeInMillis(); + + reminders.add(reminder); + } + + } catch (Exception e) { + Log.e(TAG, "Error getting reminders from database", e); + } + + return reminders; + } + + private void updateReminderInDatabase(String id, String title, String body, String time, + Boolean sound, Boolean vibration, String priority, + Boolean repeatDaily, String timezone) { + try { + SharedPreferences prefs = getContext().getSharedPreferences("daily_reminders", Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + + if (title != null) editor.putString(id + "_title", title); + if (body != null) editor.putString(id + "_body", body); + if (time != null) editor.putString(id + "_time", time); + if (sound != null) editor.putBoolean(id + "_sound", sound); + if (vibration != null) editor.putBoolean(id + "_vibration", vibration); + if (priority != null) editor.putString(id + "_priority", priority); + if (repeatDaily != null) editor.putBoolean(id + "_repeatDaily", repeatDaily); + if (timezone != null) editor.putString(id + "_timezone", timezone); + + editor.apply(); + Log.d(TAG, "Reminder updated in database: " + id); + + } catch (Exception e) { + Log.e(TAG, "Error updating reminder in database", e); + } + } + + // Data class for reminder info + public static class DailyReminderInfo { + public String id; + public String title; + public String body; + public String time; + public boolean sound; + public boolean vibration; + public String priority; + public boolean repeatDaily; + public String timezone; + public boolean isScheduled; + public long nextTriggerTime; + public long createdAt; + public long lastTriggered; + } } diff --git a/src/android/DailyNotificationScheduler.java b/src/android/DailyNotificationScheduler.java index a1f796e..2307de4 100644 --- a/src/android/DailyNotificationScheduler.java +++ b/src/android/DailyNotificationScheduler.java @@ -108,6 +108,17 @@ public class DailyNotificationScheduler { intent.setAction(ACTION_NOTIFICATION); intent.putExtra(EXTRA_NOTIFICATION_ID, content.getId()); + // Check if this is a static reminder + if (content.getId().startsWith("reminder_") || content.getId().contains("_reminder")) { + intent.putExtra("is_static_reminder", true); + intent.putExtra("reminder_id", content.getId()); + intent.putExtra("title", content.getTitle()); + intent.putExtra("body", content.getBody()); + intent.putExtra("sound", content.isSound()); + intent.putExtra("vibration", true); // Default to true for reminders + intent.putExtra("priority", content.getPriority()); + } + // Create pending intent with unique request code int requestCode = content.getId().hashCode(); PendingIntent pendingIntent = PendingIntent.getBroadcast( diff --git a/src/definitions.ts b/src/definitions.ts index 63b2ef3..7291739 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -103,6 +103,35 @@ export interface PermissionStatus { carPlay?: boolean; } +// Static Daily Reminder Interfaces +export interface DailyReminderOptions { + id: string; + title: string; + body: string; + time: string; // HH:mm format (e.g., "09:00") + sound?: boolean; + vibration?: boolean; + priority?: 'low' | 'normal' | 'high'; + repeatDaily?: boolean; + timezone?: string; +} + +export interface DailyReminderInfo { + id: string; + title: string; + body: string; + time: string; + sound: boolean; + vibration: boolean; + priority: 'low' | 'normal' | 'high'; + repeatDaily: boolean; + timezone?: string; + isScheduled: boolean; + nextTriggerTime?: number; + createdAt: number; + lastTriggered?: number; +} + export type PermissionState = 'prompt' | 'prompt-with-rationale' | 'granted' | 'denied' | 'provisional' | 'ephemeral' | 'unknown'; // Additional interfaces for enhanced functionality @@ -364,6 +393,28 @@ export interface DailyNotificationPlugin { refreshAuthenticationForNewIdentity(activeDid: string): Promise; clearCacheForNewIdentity(): Promise; updateBackgroundTaskIdentity(activeDid: string): Promise; + + // Static Daily Reminder Methods + /** + * Schedule a simple daily reminder notification + * No network content required - just static text + */ + scheduleDailyReminder(options: DailyReminderOptions): Promise; + + /** + * Cancel a daily reminder notification + */ + cancelDailyReminder(reminderId: string): Promise; + + /** + * Get all scheduled daily reminders + */ + getScheduledReminders(): Promise; + + /** + * Update an existing daily reminder + */ + updateDailyReminder(reminderId: string, options: DailyReminderOptions): Promise; } // Phase 1: TimeSafari Endorser.ch API Interfaces diff --git a/src/web.ts b/src/web.ts index 7745060..fbad0ec 100644 --- a/src/web.ts +++ b/src/web.ts @@ -811,4 +811,318 @@ export class DailyNotificationWeb extends WebPlugin implements DailyNotification throw error; } } + + // Static Daily Reminder Methods + async scheduleDailyReminder(options: any): Promise { + try { + console.log('DNP-WEB-REMINDER: Scheduling daily reminder:', options.id); + + const { id, title, body, time, sound = true, vibration = true, priority = 'normal', repeatDaily = true, timezone } = options; + + // Validate required parameters + if (!id || !title || !body || !time) { + throw new Error('Missing required parameters: id, title, body, time'); + } + + // Parse time (HH:mm format) + const timeParts = time.split(':'); + if (timeParts.length !== 2) { + throw new Error('Invalid time format. Use HH:mm (e.g., 09:00)'); + } + + const hour = parseInt(timeParts[0]); + const minute = parseInt(timeParts[1]); + + if (hour < 0 || hour > 23 || minute < 0 || minute > 59) { + throw new Error('Invalid time values. Hour must be 0-23, minute must be 0-59'); + } + + // Check if notifications are supported + if (!('Notification' in window)) { + throw new Error('This browser does not support notifications'); + } + + // Request permission if not granted + if (Notification.permission === 'default') { + const permission = await Notification.requestPermission(); + if (permission !== 'granted') { + throw new Error('Notification permission denied'); + } + } + + if (Notification.permission !== 'granted') { + throw new Error('Notification permission not granted'); + } + + // Calculate next trigger time + const now = new Date(); + const triggerTime = new Date(); + triggerTime.setHours(hour, minute, 0, 0); + + // If time has passed today, schedule for tomorrow + if (triggerTime <= now) { + triggerTime.setDate(triggerTime.getDate() + 1); + } + + const timeUntilTrigger = triggerTime.getTime() - now.getTime(); + + // Store reminder in localStorage + this.storeReminderInLocalStorage(id, title, body, time, sound, vibration, priority, repeatDaily, timezone); + + // Schedule the notification using setTimeout (simplified web implementation) + const timeoutId = setTimeout(() => { + this.showReminderNotification(title, body, sound, vibration, priority, id); + + // If repeatDaily is true, schedule the next occurrence + if (repeatDaily) { + this.scheduleDailyReminder(options); + } + }, timeUntilTrigger); + + // Store timeout ID for cancellation + this.storeReminderTimeout(id, timeoutId); + + console.log('DNP-WEB-REMINDER: Daily reminder scheduled successfully:', id); + } catch (error) { + console.error('DNP-WEB-REMINDER: Error scheduling daily reminder:', error); + throw error; + } + } + + async cancelDailyReminder(reminderId: string): Promise { + try { + console.log('DNP-WEB-REMINDER: Cancelling daily reminder:', reminderId); + + // Cancel the timeout + this.cancelReminderTimeout(reminderId); + + // Remove from localStorage + this.removeReminderFromLocalStorage(reminderId); + + console.log('DNP-WEB-REMINDER: Daily reminder cancelled:', reminderId); + } catch (error) { + console.error('DNP-WEB-REMINDER: Error cancelling daily reminder:', error); + throw error; + } + } + + async getScheduledReminders(): Promise { + try { + console.log('DNP-WEB-REMINDER: Getting scheduled reminders'); + + const reminders = this.getRemindersFromLocalStorage(); + + return { reminders }; + } catch (error) { + console.error('DNP-WEB-REMINDER: Error getting scheduled reminders:', error); + throw error; + } + } + + async updateDailyReminder(reminderId: string, options: any): Promise { + try { + console.log('DNP-WEB-REMINDER: Updating daily reminder:', reminderId); + + // Cancel existing reminder + await this.cancelDailyReminder(reminderId); + + // Update in localStorage + this.updateReminderInLocalStorage(reminderId, options); + + // Reschedule with new settings if all required fields are provided + if (options.title && options.body && options.time) { + await this.scheduleDailyReminder({ ...options, id: reminderId }); + } + + console.log('DNP-WEB-REMINDER: Daily reminder updated:', reminderId); + } catch (error) { + console.error('DNP-WEB-REMINDER: Error updating daily reminder:', error); + throw error; + } + } + + // Helper methods for web reminder functionality + private showReminderNotification( + title: string, + body: string, + sound: boolean, + vibration: boolean, + priority: string, + reminderId: string + ): void { + try { + const notification = new Notification(title, { + body: body, + icon: '/favicon.ico', // You can customize this + badge: '/favicon.ico', + tag: `reminder_${reminderId}`, + requireInteraction: priority === 'high', + silent: !sound + }); + + // Handle notification click + notification.onclick = () => { + console.log('DNP-WEB-REMINDER: Reminder notification clicked:', reminderId); + notification.close(); + }; + + // Handle notification close + notification.onclose = () => { + console.log('DNP-WEB-REMINDER: Reminder notification closed:', reminderId); + }; + + // Handle notification error + notification.onerror = (error) => { + console.error('DNP-WEB-REMINDER: Reminder notification error:', error); + }; + + // Auto-close after 10 seconds for non-high priority + if (priority !== 'high') { + setTimeout(() => { + notification.close(); + }, 10000); + } + + // Trigger vibration if supported and enabled + if (vibration && 'vibrate' in navigator) { + navigator.vibrate([200, 100, 200]); + } + + // Record reminder trigger + this.recordReminderTrigger(reminderId); + + console.log('DNP-WEB-REMINDER: Reminder notification displayed:', title); + } catch (error) { + console.error('DNP-WEB-REMINDER: Error showing reminder notification:', error); + } + } + + private storeReminderInLocalStorage( + id: string, + title: string, + body: string, + time: string, + sound: boolean, + vibration: boolean, + priority: string, + repeatDaily: boolean, + timezone?: string + ): void { + try { + const reminderData = { + id, + title, + body, + time, + sound, + vibration, + priority, + repeatDaily, + timezone: timezone || '', + createdAt: Date.now(), + lastTriggered: 0 + }; + + const reminders = this.getRemindersFromLocalStorage(); + reminders.push(reminderData); + localStorage.setItem('daily_reminders', JSON.stringify(reminders)); + + console.log('DNP-WEB-REMINDER: Reminder stored:', id); + } catch (error) { + console.error('DNP-WEB-REMINDER: Error storing reminder:', error); + } + } + + private removeReminderFromLocalStorage(id: string): void { + try { + const reminders = this.getRemindersFromLocalStorage(); + const filteredReminders = reminders.filter((reminder: any) => reminder.id !== id); + localStorage.setItem('daily_reminders', JSON.stringify(filteredReminders)); + + console.log('DNP-WEB-REMINDER: Reminder removed:', id); + } catch (error) { + console.error('DNP-WEB-REMINDER: Error removing reminder:', error); + } + } + + private getRemindersFromLocalStorage(): any[] { + try { + const reminders = localStorage.getItem('daily_reminders'); + return reminders ? JSON.parse(reminders) : []; + } catch (error) { + console.error('DNP-WEB-REMINDER: Error getting reminders from localStorage:', error); + return []; + } + } + + private updateReminderInLocalStorage(id: string, options: any): void { + try { + const reminders = this.getRemindersFromLocalStorage(); + const reminderIndex = reminders.findIndex((reminder: any) => reminder.id === id); + + if (reminderIndex !== -1) { + if (options.title) reminders[reminderIndex].title = options.title; + if (options.body) reminders[reminderIndex].body = options.body; + if (options.time) reminders[reminderIndex].time = options.time; + if (options.sound !== undefined) reminders[reminderIndex].sound = options.sound; + if (options.vibration !== undefined) reminders[reminderIndex].vibration = options.vibration; + if (options.priority) reminders[reminderIndex].priority = options.priority; + if (options.repeatDaily !== undefined) reminders[reminderIndex].repeatDaily = options.repeatDaily; + if (options.timezone) reminders[reminderIndex].timezone = options.timezone; + + localStorage.setItem('daily_reminders', JSON.stringify(reminders)); + console.log('DNP-WEB-REMINDER: Reminder updated:', id); + } + } catch (error) { + console.error('DNP-WEB-REMINDER: Error updating reminder:', error); + } + } + + private storeReminderTimeout(id: string, timeoutId: number): void { + try { + const timeouts = this.getReminderTimeouts(); + timeouts[id] = timeoutId; + localStorage.setItem('daily_reminder_timeouts', JSON.stringify(timeouts)); + } catch (error) { + console.error('DNP-WEB-REMINDER: Error storing reminder timeout:', error); + } + } + + private cancelReminderTimeout(id: string): void { + try { + const timeouts = this.getReminderTimeouts(); + if (timeouts[id]) { + clearTimeout(timeouts[id]); + delete timeouts[id]; + localStorage.setItem('daily_reminder_timeouts', JSON.stringify(timeouts)); + } + } catch (error) { + console.error('DNP-WEB-REMINDER: Error cancelling reminder timeout:', error); + } + } + + private getReminderTimeouts(): Record { + try { + const timeouts = localStorage.getItem('daily_reminder_timeouts'); + return timeouts ? JSON.parse(timeouts) : {}; + } catch (error) { + console.error('DNP-WEB-REMINDER: Error getting reminder timeouts:', error); + return {}; + } + } + + private recordReminderTrigger(id: string): void { + try { + const reminders = this.getRemindersFromLocalStorage(); + const reminderIndex = reminders.findIndex((reminder: any) => reminder.id === id); + + if (reminderIndex !== -1) { + reminders[reminderIndex].lastTriggered = Date.now(); + localStorage.setItem('daily_reminders', JSON.stringify(reminders)); + console.log('DNP-WEB-REMINDER: Reminder trigger recorded:', id); + } + } catch (error) { + console.error('DNP-WEB-REMINDER: Error recording reminder trigger:', error); + } + } } diff --git a/test-apps/README.md b/test-apps/README.md index 8a54190..c69c08e 100644 --- a/test-apps/README.md +++ b/test-apps/README.md @@ -72,6 +72,7 @@ Each test app includes comprehensive UI patterns and testing capabilities: - **TimeSafari Configuration**: Test community-focused notification settings - **Endorser.ch API Integration**: Test real API patterns with pagination - **Community Notification Scheduling**: Test offers, projects, people, and items notifications +- **Static Daily Reminders**: Test simple daily notifications without network content - **Performance Monitoring**: Metrics collection and display - **Error Handling**: Comprehensive error testing - **Debug Information**: Platform-specific debug data diff --git a/test-apps/android-test/src/index.html b/test-apps/android-test/src/index.html index fa27a41..72d51e3 100644 --- a/test-apps/android-test/src/index.html +++ b/test-apps/android-test/src/index.html @@ -402,6 +402,57 @@ + + +
+

⏰ Static Daily Reminders

+
+ + + + +
+
+

Reminder Configuration

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
diff --git a/test-apps/android-test/src/index.ts b/test-apps/android-test/src/index.ts index fe8b43c..405fe94 100644 --- a/test-apps/android-test/src/index.ts +++ b/test-apps/android-test/src/index.ts @@ -469,6 +469,12 @@ class TimeSafariAndroidTestApp { document.getElementById('test-endorser-api-client')?.addEventListener('click', () => this.testEndorserAPIClient()); document.getElementById('test-notification-manager')?.addEventListener('click', () => this.testTimeSafariNotificationManager()); document.getElementById('test-phase4-integration')?.addEventListener('click', () => this.testPhase4Integration()); + + // Static Daily Reminder event listeners + document.getElementById('schedule-reminder')?.addEventListener('click', () => this.scheduleDailyReminder()); + document.getElementById('cancel-reminder')?.addEventListener('click', () => this.cancelDailyReminder()); + document.getElementById('get-reminders')?.addEventListener('click', () => this.getScheduledReminders()); + document.getElementById('update-reminder')?.addEventListener('click', () => this.updateDailyReminder()); } private async initializeUI(): Promise { @@ -1116,6 +1122,98 @@ class TimeSafariAndroidTestApp { this.errorDisplay.showError(error as Error); } } + + // Static Daily Reminder Methods + private async scheduleDailyReminder(): Promise { + try { + this.log('Scheduling daily reminder...'); + + const reminderOptions = { + id: (document.getElementById('reminder-id') as HTMLInputElement)?.value || 'morning_checkin', + title: (document.getElementById('reminder-title') as HTMLInputElement)?.value || 'Good Morning!', + body: (document.getElementById('reminder-body') as HTMLInputElement)?.value || 'Time to check your TimeSafari community updates', + time: (document.getElementById('reminder-time') as HTMLInputElement)?.value || '09:00', + sound: (document.getElementById('reminder-sound') as HTMLInputElement)?.checked ?? true, + vibration: (document.getElementById('reminder-vibration') as HTMLInputElement)?.checked ?? true, + priority: (document.getElementById('reminder-priority') as HTMLSelectElement)?.value || 'normal', + repeatDaily: (document.getElementById('reminder-repeat') as HTMLInputElement)?.checked ?? true + }; + + this.log('Reminder options:', reminderOptions); + + await this.plugin.scheduleDailyReminder(reminderOptions); + this.log('✅ Daily reminder scheduled successfully'); + this.errorDisplay.showSuccess('Daily reminder scheduled successfully!'); + + } catch (error) { + this.log('❌ Failed to schedule daily reminder:', error); + this.errorDisplay.showError(error as Error); + } + } + + private async cancelDailyReminder(): Promise { + try { + this.log('Cancelling daily reminder...'); + + const reminderId = (document.getElementById('reminder-id') as HTMLInputElement)?.value || 'morning_checkin'; + + await this.plugin.cancelDailyReminder(reminderId); + this.log('✅ Daily reminder cancelled successfully'); + this.errorDisplay.showSuccess('Daily reminder cancelled successfully!'); + + } catch (error) { + this.log('❌ Failed to cancel daily reminder:', error); + this.errorDisplay.showError(error as Error); + } + } + + private async getScheduledReminders(): Promise { + try { + this.log('Getting scheduled reminders...'); + + const result = await this.plugin.getScheduledReminders(); + this.log('✅ Scheduled reminders retrieved:', result); + + if (result.reminders && result.reminders.length > 0) { + this.errorDisplay.showSuccess(`Found ${result.reminders.length} scheduled reminders`); + console.table(result.reminders); + } else { + this.errorDisplay.showInfo('No scheduled reminders found'); + } + + } catch (error) { + this.log('❌ Failed to get scheduled reminders:', error); + this.errorDisplay.showError(error as Error); + } + } + + private async updateDailyReminder(): Promise { + try { + this.log('Updating daily reminder...'); + + const reminderId = (document.getElementById('reminder-id') as HTMLInputElement)?.value || 'morning_checkin'; + + const updateOptions = { + title: (document.getElementById('reminder-title') as HTMLInputElement)?.value, + body: (document.getElementById('reminder-body') as HTMLInputElement)?.value, + time: (document.getElementById('reminder-time') as HTMLInputElement)?.value, + sound: (document.getElementById('reminder-sound') as HTMLInputElement)?.checked, + vibration: (document.getElementById('reminder-vibration') as HTMLInputElement)?.checked, + priority: (document.getElementById('reminder-priority') as HTMLSelectElement)?.value, + repeatDaily: (document.getElementById('reminder-repeat') as HTMLInputElement)?.checked + }; + + this.log('Update options:', updateOptions); + + await this.plugin.updateDailyReminder(reminderId, updateOptions); + this.log('✅ Daily reminder updated successfully'); + this.errorDisplay.showSuccess('Daily reminder updated successfully!'); + + } catch (error) { + this.log('❌ Failed to update daily reminder:', error); + this.errorDisplay.showError(error as Error); + } + } } // Initialize app when DOM is ready diff --git a/test-apps/electron-test/src/index.html b/test-apps/electron-test/src/index.html index fda837f..605eb44 100644 --- a/test-apps/electron-test/src/index.html +++ b/test-apps/electron-test/src/index.html @@ -402,6 +402,57 @@
+ + +
+

⏰ Static Daily Reminders

+
+ + + + +
+
+

Reminder Configuration

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
diff --git a/test-apps/electron-test/src/index.ts b/test-apps/electron-test/src/index.ts index fb1530a..996a466 100644 --- a/test-apps/electron-test/src/index.ts +++ b/test-apps/electron-test/src/index.ts @@ -318,6 +318,12 @@ class TimeSafariElectronTestApp { document.getElementById('test-endorser-api-client')?.addEventListener('click', () => this.testEndorserAPIClient()); document.getElementById('test-notification-manager')?.addEventListener('click', () => this.testTimeSafariNotificationManager()); document.getElementById('test-phase4-integration')?.addEventListener('click', () => this.testPhase4Integration()); + + // Static Daily Reminder event listeners + document.getElementById('schedule-reminder')?.addEventListener('click', () => this.scheduleDailyReminder()); + document.getElementById('cancel-reminder')?.addEventListener('click', () => this.cancelDailyReminder()); + document.getElementById('get-reminders')?.addEventListener('click', () => this.getScheduledReminders()); + document.getElementById('update-reminder')?.addEventListener('click', () => this.updateDailyReminder()); } private async initializeUI(): Promise { @@ -963,6 +969,98 @@ class TimeSafariElectronTestApp { this.errorDisplay.showError(error as Error); } } + + // Static Daily Reminder Methods + private async scheduleDailyReminder(): Promise { + try { + this.log('Scheduling daily reminder...'); + + const reminderOptions = { + id: (document.getElementById('reminder-id') as HTMLInputElement)?.value || 'morning_checkin', + title: (document.getElementById('reminder-title') as HTMLInputElement)?.value || 'Good Morning!', + body: (document.getElementById('reminder-body') as HTMLInputElement)?.value || 'Time to check your TimeSafari community updates', + time: (document.getElementById('reminder-time') as HTMLInputElement)?.value || '09:00', + sound: (document.getElementById('reminder-sound') as HTMLInputElement)?.checked ?? true, + vibration: (document.getElementById('reminder-vibration') as HTMLInputElement)?.checked ?? true, + priority: (document.getElementById('reminder-priority') as HTMLSelectElement)?.value || 'normal', + repeatDaily: (document.getElementById('reminder-repeat') as HTMLInputElement)?.checked ?? true + }; + + this.log('Reminder options:', reminderOptions); + + await this.plugin.scheduleDailyReminder(reminderOptions); + this.log('✅ Daily reminder scheduled successfully'); + this.errorDisplay.showSuccess('Daily reminder scheduled successfully!'); + + } catch (error) { + this.log('❌ Failed to schedule daily reminder:', error); + this.errorDisplay.showError(error as Error); + } + } + + private async cancelDailyReminder(): Promise { + try { + this.log('Cancelling daily reminder...'); + + const reminderId = (document.getElementById('reminder-id') as HTMLInputElement)?.value || 'morning_checkin'; + + await this.plugin.cancelDailyReminder(reminderId); + this.log('✅ Daily reminder cancelled successfully'); + this.errorDisplay.showSuccess('Daily reminder cancelled successfully!'); + + } catch (error) { + this.log('❌ Failed to cancel daily reminder:', error); + this.errorDisplay.showError(error as Error); + } + } + + private async getScheduledReminders(): Promise { + try { + this.log('Getting scheduled reminders...'); + + const result = await this.plugin.getScheduledReminders(); + this.log('✅ Scheduled reminders retrieved:', result); + + if (result.reminders && result.reminders.length > 0) { + this.errorDisplay.showSuccess(`Found ${result.reminders.length} scheduled reminders`); + console.table(result.reminders); + } else { + this.errorDisplay.showInfo('No scheduled reminders found'); + } + + } catch (error) { + this.log('❌ Failed to get scheduled reminders:', error); + this.errorDisplay.showError(error as Error); + } + } + + private async updateDailyReminder(): Promise { + try { + this.log('Updating daily reminder...'); + + const reminderId = (document.getElementById('reminder-id') as HTMLInputElement)?.value || 'morning_checkin'; + + const updateOptions = { + title: (document.getElementById('reminder-title') as HTMLInputElement)?.value, + body: (document.getElementById('reminder-body') as HTMLInputElement)?.value, + time: (document.getElementById('reminder-time') as HTMLInputElement)?.value, + sound: (document.getElementById('reminder-sound') as HTMLInputElement)?.checked, + vibration: (document.getElementById('reminder-vibration') as HTMLInputElement)?.checked, + priority: (document.getElementById('reminder-priority') as HTMLSelectElement)?.value, + repeatDaily: (document.getElementById('reminder-repeat') as HTMLInputElement)?.checked + }; + + this.log('Update options:', updateOptions); + + await this.plugin.updateDailyReminder(reminderId, updateOptions); + this.log('✅ Daily reminder updated successfully'); + this.errorDisplay.showSuccess('Daily reminder updated successfully!'); + + } catch (error) { + this.log('❌ Failed to update daily reminder:', error); + this.errorDisplay.showError(error as Error); + } + } } // Initialize app when DOM is ready diff --git a/test-apps/ios-test/src/index.html b/test-apps/ios-test/src/index.html index e1dad73..ab1241f 100644 --- a/test-apps/ios-test/src/index.html +++ b/test-apps/ios-test/src/index.html @@ -402,6 +402,57 @@
+ + +
+

⏰ Static Daily Reminders

+
+ + + + +
+
+

Reminder Configuration

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
diff --git a/test-apps/ios-test/src/index.ts b/test-apps/ios-test/src/index.ts index 93cfcb0..a41cbd1 100644 --- a/test-apps/ios-test/src/index.ts +++ b/test-apps/ios-test/src/index.ts @@ -318,6 +318,12 @@ class TimeSafariIOSTestApp { document.getElementById('test-endorser-api-client')?.addEventListener('click', () => this.testEndorserAPIClient()); document.getElementById('test-notification-manager')?.addEventListener('click', () => this.testTimeSafariNotificationManager()); document.getElementById('test-phase4-integration')?.addEventListener('click', () => this.testPhase4Integration()); + + // Static Daily Reminder event listeners + document.getElementById('schedule-reminder')?.addEventListener('click', () => this.scheduleDailyReminder()); + document.getElementById('cancel-reminder')?.addEventListener('click', () => this.cancelDailyReminder()); + document.getElementById('get-reminders')?.addEventListener('click', () => this.getScheduledReminders()); + document.getElementById('update-reminder')?.addEventListener('click', () => this.updateDailyReminder()); } private async initializeUI(): Promise { @@ -960,6 +966,98 @@ class TimeSafariIOSTestApp { this.errorDisplay.showError(error as Error); } } + + // Static Daily Reminder Methods + private async scheduleDailyReminder(): Promise { + try { + this.log('Scheduling daily reminder...'); + + const reminderOptions = { + id: (document.getElementById('reminder-id') as HTMLInputElement)?.value || 'morning_checkin', + title: (document.getElementById('reminder-title') as HTMLInputElement)?.value || 'Good Morning!', + body: (document.getElementById('reminder-body') as HTMLInputElement)?.value || 'Time to check your TimeSafari community updates', + time: (document.getElementById('reminder-time') as HTMLInputElement)?.value || '09:00', + sound: (document.getElementById('reminder-sound') as HTMLInputElement)?.checked ?? true, + vibration: (document.getElementById('reminder-vibration') as HTMLInputElement)?.checked ?? true, + priority: (document.getElementById('reminder-priority') as HTMLSelectElement)?.value || 'normal', + repeatDaily: (document.getElementById('reminder-repeat') as HTMLInputElement)?.checked ?? true + }; + + this.log('Reminder options:', reminderOptions); + + await this.plugin.scheduleDailyReminder(reminderOptions); + this.log('✅ Daily reminder scheduled successfully'); + this.errorDisplay.showSuccess('Daily reminder scheduled successfully!'); + + } catch (error) { + this.log('❌ Failed to schedule daily reminder:', error); + this.errorDisplay.showError(error as Error); + } + } + + private async cancelDailyReminder(): Promise { + try { + this.log('Cancelling daily reminder...'); + + const reminderId = (document.getElementById('reminder-id') as HTMLInputElement)?.value || 'morning_checkin'; + + await this.plugin.cancelDailyReminder(reminderId); + this.log('✅ Daily reminder cancelled successfully'); + this.errorDisplay.showSuccess('Daily reminder cancelled successfully!'); + + } catch (error) { + this.log('❌ Failed to cancel daily reminder:', error); + this.errorDisplay.showError(error as Error); + } + } + + private async getScheduledReminders(): Promise { + try { + this.log('Getting scheduled reminders...'); + + const result = await this.plugin.getScheduledReminders(); + this.log('✅ Scheduled reminders retrieved:', result); + + if (result.reminders && result.reminders.length > 0) { + this.errorDisplay.showSuccess(`Found ${result.reminders.length} scheduled reminders`); + console.table(result.reminders); + } else { + this.errorDisplay.showInfo('No scheduled reminders found'); + } + + } catch (error) { + this.log('❌ Failed to get scheduled reminders:', error); + this.errorDisplay.showError(error as Error); + } + } + + private async updateDailyReminder(): Promise { + try { + this.log('Updating daily reminder...'); + + const reminderId = (document.getElementById('reminder-id') as HTMLInputElement)?.value || 'morning_checkin'; + + const updateOptions = { + title: (document.getElementById('reminder-title') as HTMLInputElement)?.value, + body: (document.getElementById('reminder-body') as HTMLInputElement)?.value, + time: (document.getElementById('reminder-time') as HTMLInputElement)?.value, + sound: (document.getElementById('reminder-sound') as HTMLInputElement)?.checked, + vibration: (document.getElementById('reminder-vibration') as HTMLInputElement)?.checked, + priority: (document.getElementById('reminder-priority') as HTMLSelectElement)?.value, + repeatDaily: (document.getElementById('reminder-repeat') as HTMLInputElement)?.checked + }; + + this.log('Update options:', updateOptions); + + await this.plugin.updateDailyReminder(reminderId, updateOptions); + this.log('✅ Daily reminder updated successfully'); + this.errorDisplay.showSuccess('Daily reminder updated successfully!'); + + } catch (error) { + this.log('❌ Failed to update daily reminder:', error); + this.errorDisplay.showError(error as Error); + } + } } // Initialize app when DOM is ready diff --git a/tests/advanced-scenarios.test.ts b/tests/advanced-scenarios.test.ts index f45fa8f..5034eda 100644 --- a/tests/advanced-scenarios.test.ts +++ b/tests/advanced-scenarios.test.ts @@ -56,6 +56,12 @@ describe('DailyNotification Advanced Scenarios', () => { refreshAuthenticationForNewIdentity: jest.fn(), clearCacheForNewIdentity: jest.fn(), updateBackgroundTaskIdentity: jest.fn(), + + // Static Daily Reminder Methods + scheduleDailyReminder: jest.fn(), + cancelDailyReminder: jest.fn(), + getScheduledReminders: jest.fn(), + updateDailyReminder: jest.fn(), }; plugin = new DailyNotification(mockPlugin); }); diff --git a/tests/daily-notification.test.ts b/tests/daily-notification.test.ts index 72b30e4..f64f13a 100644 --- a/tests/daily-notification.test.ts +++ b/tests/daily-notification.test.ts @@ -70,6 +70,12 @@ describe('DailyNotification Plugin', () => { refreshAuthenticationForNewIdentity: jest.fn(), clearCacheForNewIdentity: jest.fn(), updateBackgroundTaskIdentity: jest.fn(), + + // Static Daily Reminder Methods + scheduleDailyReminder: jest.fn(), + cancelDailyReminder: jest.fn(), + getScheduledReminders: jest.fn(), + updateDailyReminder: jest.fn(), }; // Create plugin instance with mock diff --git a/tests/edge-cases.test.ts b/tests/edge-cases.test.ts index 9769b4d..83c3a51 100644 --- a/tests/edge-cases.test.ts +++ b/tests/edge-cases.test.ts @@ -61,6 +61,12 @@ describe('DailyNotification Edge Cases', () => { refreshAuthenticationForNewIdentity: jest.fn(), clearCacheForNewIdentity: jest.fn(), updateBackgroundTaskIdentity: jest.fn(), + + // Static Daily Reminder Methods + scheduleDailyReminder: jest.fn(), + cancelDailyReminder: jest.fn(), + getScheduledReminders: jest.fn(), + updateDailyReminder: jest.fn(), }; plugin = new DailyNotification(mockPlugin); }); diff --git a/tests/enterprise-scenarios.test.ts b/tests/enterprise-scenarios.test.ts index ca51530..64bb513 100644 --- a/tests/enterprise-scenarios.test.ts +++ b/tests/enterprise-scenarios.test.ts @@ -60,6 +60,12 @@ describe('DailyNotification Enterprise Scenarios', () => { refreshAuthenticationForNewIdentity: jest.fn(), clearCacheForNewIdentity: jest.fn(), updateBackgroundTaskIdentity: jest.fn(), + + // Static Daily Reminder Methods + scheduleDailyReminder: jest.fn(), + cancelDailyReminder: jest.fn(), + getScheduledReminders: jest.fn(), + updateDailyReminder: jest.fn(), }; plugin = new DailyNotification(mockPlugin); });