29 KiB
Vue3 Notification Implementation Guide
Author: Matthew Raymer
Date: October 20, 2025
Version: 1.0.0
Status: Implementation Planning
Overview
This document provides a comprehensive guide for implementing scheduled local notifications and API-based change detection in the Vue3 example app using the DailyNotification plugin. It covers both basic notification scheduling and advanced TimeSafari API integration.
Table of Contents
- Current Plugin Capabilities
- Implementation Strategy
- Architecture Design
- Implementation Workflow
- UI/UX Considerations
- Technical Considerations
- Success Metrics
- Code Examples
- Testing Strategy
- Deployment Checklist
Current Plugin Capabilities
Available Scheduling Methods
The DailyNotification
plugin provides several key methods for scheduling:
1. Basic Scheduling Methods
scheduleDailyNotification(options)
- Simple daily notificationsscheduleDailyReminder(options)
- More advanced reminder systemscheduleContentFetch(config)
- Background content fetchingscheduleUserNotification(config)
- User-facing notificationsscheduleDualNotification(config)
- Combined content fetch + user notification
2. Available Parameters
Basic Notification Options:
interface BasicNotificationOptions {
time: string; // HH:mm format (e.g., "09:00")
title: string; // Notification title
body: string; // Notification message
sound?: boolean; // Enable sound (default: true)
priority?: string; // Priority level (default: "default")
url?: string; // Optional deep link
}
Advanced Reminder Options:
interface AdvancedReminderOptions {
id: string; // Unique reminder identifier
title: string; // Reminder title
body: string; // Reminder message
time: string; // HH:mm format
sound?: boolean; // Enable sound (default: true)
vibration?: boolean; // Enable vibration (default: true)
priority?: string; // Priority level (default: "normal")
repeatDaily?: boolean; // Repeat daily (default: true)
timezone?: string; // Timezone (default: "UTC")
}
Implementation Strategy
Phase 1: Basic Scheduled Local Notifications
1.1 Enhance ScheduleView.vue
Current State:
// Current TODO in ScheduleView.vue line 55:
// TODO: call plugin
Implementation:
async scheduleNotification() {
this.isScheduling = true
try {
const { DailyNotification } = await import('@timesafari/daily-notification-plugin')
await DailyNotification.scheduleDailyNotification({
time: this.scheduleTime,
title: this.notificationTitle,
body: this.notificationMessage,
sound: true,
priority: "default"
})
// Show success message
this.showSuccessMessage('Notification scheduled successfully!')
} catch (error) {
console.error('Failed to schedule notification:', error)
this.showErrorMessage('Failed to schedule notification: ' + error.message)
} finally {
this.isScheduling = false
}
}
1.2 Create Reminder Management System
New Component: ReminderManager.vue
interface Reminder {
id: string
title: string
body: string
time: string
enabled: boolean
lastTriggered?: number
createdAt: number
updatedAt: number
}
// Methods for managing reminders:
- scheduleReminder(reminder: Reminder)
- cancelReminder(id: string)
- updateReminder(id: string, updates: Partial<Reminder>)
- getScheduledReminders()
- toggleReminder(id: string, enabled: boolean)
Phase 2: API-Based Change Detection
2.1 TimeSafari Integration Service
New Service: TimeSafariApiService.ts
class TimeSafariApiService {
private integrationService: TimeSafariIntegrationService
private activeDid: string = ''
private apiServer: string = 'https://endorser.ch'
async initialize(activeDid: string, apiServer: string) {
this.activeDid = activeDid
this.apiServer = apiServer
this.integrationService = TimeSafariIntegrationService.getInstance()
await this.integrationService.initialize({
activeDid,
storageAdapter: this.getStorageAdapter(),
endorserApiBaseUrl: apiServer,
starredProjectsConfig: {
enabled: true,
starredPlanHandleIds: [],
lastAckedJwtId: '',
fetchInterval: '0 8 * * *' // Daily at 8 AM
}
})
}
async checkForChanges(): Promise<ChangeNotification[]> {
try {
const changes = await this.integrationService.getStarredProjectsWithChanges(
this.activeDid,
this.starredPlanHandleIds,
this.lastAckedJwtId
)
return this.mapToChangeNotifications(changes)
} catch (error) {
console.error('Failed to check for changes:', error)
return []
}
}
private mapToChangeNotifications(changes: any): ChangeNotification[] {
// Map API response to change notifications
return changes.data.map((change: any) => ({
id: change.id,
type: this.determineChangeType(change),
title: change.title || 'Project Update',
message: change.description || 'A project you follow has been updated',
timestamp: Date.now(),
actionUrl: change.url
}))
}
}
2.2 Change Detection Workflow
Change Notification Interface:
interface ChangeNotification {
id: string
type: 'offer' | 'project_update' | 'new_plan' | 'status_change'
title: string
message: string
timestamp: number
actionUrl?: string
read: boolean
dismissed: boolean
}
Implementation in HomeView.vue:
async checkForApiChanges() {
try {
const changes = await this.timeSafariService.checkForChanges()
if (changes.length > 0) {
// Show notification for each change
for (const change of changes) {
await this.showChangeNotification(change)
}
// Update UI
this.updateChangeIndicator(changes.length)
// Store changes for history
this.storeChangeHistory(changes)
}
} catch (error) {
console.error('Failed to check for changes:', error)
this.showErrorMessage('Failed to check for changes')
}
}
async showChangeNotification(change: ChangeNotification) {
try {
await DailyNotification.scheduleUserNotification({
schedule: 'immediate',
title: change.title,
body: change.message,
actions: [
{ id: 'view', title: 'View' },
{ id: 'dismiss', title: 'Dismiss' }
]
})
} catch (error) {
console.error('Failed to show change notification:', error)
}
}
Architecture Design
Service Layer Structure
src/services/
├── NotificationService.ts // Basic notification management
├── TimeSafariApiService.ts // API integration
├── ChangeDetectionService.ts // Change detection logic
├── ReminderService.ts // Reminder management
└── StorageService.ts // Local storage management
Vue Components Enhancement
src/views/
├── ScheduleView.vue // Enhanced with plugin integration
├── NotificationsView.vue // Full notification management
├── ChangeDetectionView.vue // New: API change monitoring
├── ReminderManagerView.vue // New: Reminder management
└── ChangeHistoryView.vue // New: Change history display
Store Integration
Enhanced App Store:
interface AppState {
// Existing properties
isLoading: boolean
errorMessage: string | null
platform: string
isNative: boolean
notificationStatus: NotificationStatus | null
// New properties for notifications
scheduledReminders: Reminder[]
pendingChanges: ChangeNotification[]
changeHistory: ChangeNotification[]
apiConnectionStatus: 'connected' | 'disconnected' | 'error'
lastChangeCheck: number
notificationPreferences: NotificationPreferences
}
interface NotificationPreferences {
enableScheduledReminders: boolean
enableChangeNotifications: boolean
enableSystemNotifications: boolean
quietHoursStart: string
quietHoursEnd: string
preferredNotificationTimes: string[]
changeTypes: string[]
}
Implementation Workflow
Step 1: Basic Notification Scheduling
-
Enhance ScheduleView.vue
- Implement the TODO with plugin calls
- Add form validation for time format and required fields
- Add success/error message handling
- Test scheduling functionality
-
Add Error Handling
- Show user-friendly error messages
- Handle permission denials gracefully
- Provide fallback options when plugin unavailable
-
Add Validation
- Ensure time format is HH:mm
- Validate required fields (title, body, time)
- Check for duplicate reminders
-
Test Scheduling
- Verify notifications appear at scheduled times
- Test on both Android and iOS platforms
- Verify notification content and actions
Step 2: Reminder Management
-
Create ReminderService
- Implement CRUD operations for reminders
- Add persistence using app store
- Handle reminder state management
-
Build ReminderManagerView
- Create UI for managing reminders
- Add reminder creation/editing forms
- Implement reminder list with actions
-
Add Persistence
- Store reminders in app store
- Add localStorage backup
- Handle data migration
-
Implement Cancellation
- Allow users to cancel/modify reminders
- Handle reminder updates
- Add bulk operations
Step 3: API Integration
-
Create TimeSafariApiService
- Implement API communication
- Handle authentication (JWT/DID)
- Add error handling and retry logic
-
Implement Change Detection
- Poll for changes at configurable intervals
- Handle different change types
- Implement change filtering
-
Add Change Notifications
- Show notifications when changes detected
- Handle notification actions
- Track notification engagement
-
Add Change History
- Store change history
- Provide history viewing interface
- Implement change management (mark as read, dismiss)
Step 4: Advanced Features
-
Background Sync
- Use plugin's background fetching capabilities
- Implement efficient polling strategies
- Handle offline scenarios
-
Smart Scheduling
- Adapt to user behavior patterns
- Implement intelligent notification timing
- Add user preference learning
-
Change Filtering
- Allow users to filter change types
- Implement notification preferences
- Add quiet hours functionality
-
Analytics
- Track notification effectiveness
- Monitor user engagement
- Collect performance metrics
UI/UX Considerations
Notification Types
-
Scheduled Reminders
- User-initiated, recurring notifications
- Customizable timing and content
- User-controlled enable/disable
-
Change Notifications
- API-triggered, immediate notifications
- Contextual information and actions
- Batch handling for multiple changes
-
System Notifications
- Plugin status updates
- Error notifications
- Permission requests
User Controls
-
Enable/Disable Options
- Toggle different notification types
- Granular control over notification sources
- Quick enable/disable for all notifications
-
Time Preferences
- Set preferred notification times
- Configure quiet hours
- Timezone handling
-
Change Filters
- Choose which changes to be notified about
- Filter by change type or source
- Set notification frequency limits
-
Visual Customization
- Custom notification sounds
- Vibration patterns
- Notification appearance options
Visual Indicators
-
Badge Counts
- Show pending changes count
- Display unread notifications
- Update in real-time
-
Status Indicators
- API connection status
- Plugin availability status
- Last sync time
-
History Views
- Show past notifications
- Display change history
- Provide search and filtering
Technical Considerations
Plugin Integration Points
-
Permission Handling
- Request notification permissions on app start
- Handle permission denials gracefully
- Provide permission request retry mechanisms
-
Platform Differences
- Handle Android/iOS notification differences
- Adapt to platform-specific limitations
- Test on multiple device types
-
Error Recovery
- Handle plugin failures gracefully
- Implement fallback notification methods
- Provide user feedback on errors
-
Background Execution
- Ensure background tasks work correctly
- Handle app lifecycle events
- Test background notification delivery
API Integration Challenges
-
Authentication
- Handle JWT/DID authentication
- Manage token refresh
- Handle authentication failures
-
Rate Limiting
- Respect API rate limits
- Implement exponential backoff
- Handle rate limit exceeded errors
-
Offline Handling
- Cache changes when offline
- Sync when connection restored
- Handle offline notification scenarios
-
Error Handling
- Graceful degradation on API failures
- Retry mechanisms for transient errors
- User feedback for persistent errors
Performance Optimization
-
Efficient Polling
- Minimize API calls while maintaining responsiveness
- Implement smart polling intervals
- Use push notifications when available
-
Caching
- Cache API responses appropriately
- Implement cache invalidation strategies
- Handle cache size limits
-
Background Processing
- Use plugin's background capabilities
- Minimize foreground processing
- Optimize for battery life
-
Memory Management
- Clean up old notifications/changes
- Implement data retention policies
- Monitor memory usage
Success Metrics
Notification Reliability
-
Delivery Rate
- Percentage of notifications delivered successfully
- Track delivery failures and reasons
- Monitor platform-specific delivery rates
-
Timing Accuracy
- How close notifications appear to scheduled time
- Track timing variations across platforms
- Monitor system clock accuracy impact
-
User Engagement
- Click-through rates on notifications
- User interaction with notification actions
- Notification dismissal patterns
Change Detection Effectiveness
-
Detection Speed
- Time from change occurrence to notification
- API response time monitoring
- Background processing efficiency
-
False Positives
- Unwanted or irrelevant notifications
- User feedback on notification relevance
- Filter effectiveness
-
User Satisfaction
- User feedback on notification usefulness
- Preference setting usage
- Notification frequency satisfaction
System Performance
-
API Efficiency
- Minimal API calls for maximum coverage
- Request/response size optimization
- Network usage monitoring
-
Battery Impact
- Battery drain from background processing
- Notification frequency impact
- Platform-specific optimization
-
Storage Usage
- Efficient data storage
- Cache size management
- Data retention policy effectiveness
Code Examples
Complete ScheduleView.vue Implementation
<template>
<div class="schedule-view">
<div class="view-header">
<h1 class="page-title">📅 Schedule Notification</h1>
<p class="page-subtitle">Schedule a new daily notification</p>
</div>
<div class="schedule-form">
<div class="form-group">
<label class="form-label">Notification Time</label>
<input
type="time"
class="form-input"
v-model="scheduleTime"
:class="{ 'error': timeError }"
/>
<div v-if="timeError" class="error-message">{{ timeError }}</div>
</div>
<div class="form-group">
<label class="form-label">Title</label>
<input
type="text"
class="form-input"
v-model="notificationTitle"
placeholder="Daily Update"
:class="{ 'error': titleError }"
/>
<div v-if="titleError" class="error-message">{{ titleError }}</div>
</div>
<div class="form-group">
<label class="form-label">Message</label>
<textarea
class="form-textarea"
v-model="notificationMessage"
placeholder="Your daily notification message"
:class="{ 'error': messageError }"
></textarea>
<div v-if="messageError" class="error-message">{{ messageError }}</div>
</div>
<div class="form-group">
<label class="form-label">
<input type="checkbox" v-model="enableSound" />
Enable Sound
</label>
</div>
<div class="form-group">
<label class="form-label">
<input type="checkbox" v-model="repeatDaily" />
Repeat Daily
</label>
</div>
<div class="form-actions">
<button
class="action-button primary"
@click="scheduleNotification"
:disabled="isScheduling || !isFormValid"
>
{{ isScheduling ? 'Scheduling...' : 'Schedule Notification' }}
</button>
</div>
<div v-if="successMessage" class="success-message">
{{ successMessage }}
</div>
<div v-if="errorMessage" class="error-message">
{{ errorMessage }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useAppStore } from '@/stores/app'
const appStore = useAppStore()
// Form data
const scheduleTime = ref('09:00')
const notificationTitle = ref('Daily Update')
const notificationMessage = ref('Your daily notification is ready!')
const enableSound = ref(true)
const repeatDaily = ref(true)
// State
const isScheduling = ref(false)
const successMessage = ref('')
const errorMessage = ref('')
// Validation
const timeError = ref('')
const titleError = ref('')
const messageError = ref('')
// Computed
const isFormValid = computed(() => {
return scheduleTime.value &&
notificationTitle.value.trim() &&
notificationMessage.value.trim() &&
!timeError.value &&
!titleError.value &&
!messageError.value
})
// Methods
const validateForm = () => {
timeError.value = ''
titleError.value = ''
messageError.value = ''
// Validate time format
if (!scheduleTime.value) {
timeError.value = 'Time is required'
} else if (!/^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$/.test(scheduleTime.value)) {
timeError.value = 'Invalid time format. Use HH:MM'
}
// Validate title
if (!notificationTitle.value.trim()) {
titleError.value = 'Title is required'
} else if (notificationTitle.value.trim().length > 100) {
titleError.value = 'Title must be less than 100 characters'
}
// Validate message
if (!notificationMessage.value.trim()) {
messageError.value = 'Message is required'
} else if (notificationMessage.value.trim().length > 500) {
messageError.value = 'Message must be less than 500 characters'
}
}
const clearMessages = () => {
successMessage.value = ''
errorMessage.value = ''
}
const scheduleNotification = async () => {
validateForm()
if (!isFormValid.value) {
return
}
isScheduling.value = true
clearMessages()
try {
const { DailyNotification } = await import('@timesafari/daily-notification-plugin')
// Check if plugin is available
if (!DailyNotification) {
throw new Error('DailyNotification plugin not available')
}
// Schedule the notification
await DailyNotification.scheduleDailyNotification({
time: scheduleTime.value,
title: notificationTitle.value.trim(),
body: notificationMessage.value.trim(),
sound: enableSound.value,
priority: "default"
})
successMessage.value = 'Notification scheduled successfully!'
// Reset form
scheduleTime.value = '09:00'
notificationTitle.value = 'Daily Update'
notificationMessage.value = 'Your daily notification is ready!'
} catch (error) {
console.error('Failed to schedule notification:', error)
errorMessage.value = `Failed to schedule notification: ${error.message}`
} finally {
isScheduling.value = false
}
}
// Watch for form changes to clear messages
watch([scheduleTime, notificationTitle, notificationMessage], () => {
clearMessages()
})
onMounted(() => {
// Check if notifications are available
if (!appStore.isNotificationReady) {
errorMessage.value = 'Notifications are not available. Please check your settings.'
}
})
</script>
<style scoped>
/* ... existing styles ... */
.error-message {
color: #f44336;
font-size: 12px;
margin-top: 4px;
}
.success-message {
color: #4caf50;
font-size: 14px;
margin-top: 16px;
padding: 8px;
background: rgba(76, 175, 80, 0.1);
border-radius: 4px;
}
.form-input.error,
.form-textarea.error {
border-color: #f44336;
}
.form-label {
display: flex;
align-items: center;
gap: 8px;
}
</style>
TimeSafariApiService Implementation
/**
* TimeSafari API Service
*
* Handles integration with TimeSafari API for change detection
* and notification management.
*
* @author Matthew Raymer
* @version 1.0.0
*/
import { TimeSafariIntegrationService } from '@timesafari/daily-notification-plugin'
export interface ChangeNotification {
id: string
type: 'offer' | 'project_update' | 'new_plan' | 'status_change'
title: string
message: string
timestamp: number
actionUrl?: string
read: boolean
dismissed: boolean
}
export interface TimeSafariConfig {
activeDid: string
apiServer: string
starredPlanHandleIds: string[]
lastAckedJwtId: string
pollingInterval: number
}
export class TimeSafariApiService {
private integrationService: TimeSafariIntegrationService | null = null
private config: TimeSafariConfig | null = null
private pollingTimer: NodeJS.Timeout | null = null
private isPolling = false
async initialize(config: TimeSafariConfig): Promise<void> {
try {
this.config = config
this.integrationService = TimeSafariIntegrationService.getInstance()
await this.integrationService.initialize({
activeDid: config.activeDid,
storageAdapter: this.getStorageAdapter(),
endorserApiBaseUrl: config.apiServer,
starredProjectsConfig: {
enabled: true,
starredPlanHandleIds: config.starredPlanHandleIds,
lastAckedJwtId: config.lastAckedJwtId,
fetchInterval: '0 8 * * *' // Daily at 8 AM
}
})
console.log('TimeSafari API Service initialized successfully')
} catch (error) {
console.error('Failed to initialize TimeSafari API Service:', error)
throw error
}
}
async checkForChanges(): Promise<ChangeNotification[]> {
if (!this.integrationService || !this.config) {
throw new Error('Service not initialized')
}
try {
const changes = await this.integrationService.getStarredProjectsWithChanges(
this.config.activeDid,
this.config.starredPlanHandleIds,
this.config.lastAckedJwtId
)
return this.mapToChangeNotifications(changes)
} catch (error) {
console.error('Failed to check for changes:', error)
return []
}
}
startPolling(callback: (changes: ChangeNotification[]) => void): void {
if (this.isPolling) {
return
}
this.isPolling = true
const poll = async () => {
try {
const changes = await this.checkForChanges()
if (changes.length > 0) {
callback(changes)
}
} catch (error) {
console.error('Polling error:', error)
}
}
// Initial poll
poll()
// Set up interval
this.pollingTimer = setInterval(poll, this.config?.pollingInterval || 300000) // 5 minutes default
}
stopPolling(): void {
if (this.pollingTimer) {
clearInterval(this.pollingTimer)
this.pollingTimer = null
}
this.isPolling = false
}
private mapToChangeNotifications(changes: any): ChangeNotification[] {
if (!changes.data || !Array.isArray(changes.data)) {
return []
}
return changes.data.map((change: any) => ({
id: change.id || `change_${Date.now()}_${Math.random()}`,
type: this.determineChangeType(change),
title: change.title || 'Project Update',
message: change.description || 'A project you follow has been updated',
timestamp: Date.now(),
actionUrl: change.url,
read: false,
dismissed: false
}))
}
private determineChangeType(change: any): ChangeNotification['type'] {
if (change.type) {
return change.type
}
// Determine type based on content
if (change.title?.toLowerCase().includes('offer')) {
return 'offer'
} else if (change.title?.toLowerCase().includes('project')) {
return 'project_update'
} else if (change.title?.toLowerCase().includes('new')) {
return 'new_plan'
} else {
return 'status_change'
}
}
private getStorageAdapter(): any {
// Return appropriate storage adapter
return {
store: (key: string, value: any, ttl?: number) => {
// Implementation depends on platform
if (typeof localStorage !== 'undefined') {
localStorage.setItem(key, JSON.stringify({ value, ttl, timestamp: Date.now() }))
}
},
retrieve: (key: string) => {
if (typeof localStorage !== 'undefined') {
const item = localStorage.getItem(key)
if (item) {
const { value, ttl, timestamp } = JSON.parse(item)
if (ttl && Date.now() - timestamp > ttl) {
localStorage.removeItem(key)
return null
}
return value
}
}
return null
}
}
}
}
export default TimeSafariApiService
Testing Strategy
Unit Tests
-
Service Layer Testing
- Test notification scheduling logic
- Test API integration methods
- Test error handling scenarios
-
Component Testing
- Test form validation
- Test user interactions
- Test state management
-
Plugin Integration Testing
- Test plugin method calls
- Test permission handling
- Test platform differences
Integration Tests
-
End-to-End Notification Flow
- Test complete notification scheduling
- Test notification delivery
- Test user interactions
-
API Integration Flow
- Test change detection
- Test notification triggering
- Test error scenarios
-
Cross-Platform Testing
- Test on Android devices
- Test on iOS devices
- Test on web browsers
Manual Testing
-
Device Testing
- Test on various Android versions
- Test on various iOS versions
- Test on different screen sizes
-
Network Testing
- Test with poor connectivity
- Test offline scenarios
- Test API failures
-
User Experience Testing
- Test notification timing
- Test user interface responsiveness
- Test error message clarity
Deployment Checklist
Pre-Deployment
- All unit tests passing
- Integration tests passing
- Manual testing completed
- Code review completed
- Documentation updated
- Performance testing completed
Deployment Steps
-
Build Process
- Run
npm run build
- Run
npx cap sync android
- Run fix script for capacitor.plugins.json
- Verify plugin registration
- Run
-
Testing
- Test on Android device
- Test notification scheduling
- Test API integration
- Test error handling
-
Monitoring
- Monitor notification delivery rates
- Monitor API response times
- Monitor error rates
- Monitor user feedback
Post-Deployment
- Monitor system performance
- Collect user feedback
- Track success metrics
- Plan future improvements
Conclusion
This implementation guide provides a comprehensive roadmap for adding scheduled local notifications and API-based change detection to the Vue3 example app. The approach leverages the existing DailyNotification plugin capabilities while building a robust, user-friendly notification system.
Key benefits of this implementation:
- Native-First Approach: Uses platform-specific notification systems for reliable delivery
- Offline-First Design: Works even when the app is closed or offline
- User-Controlled: Provides granular control over notification preferences
- Scalable Architecture: Supports future enhancements and additional notification types
- Cross-Platform: Works consistently across Android, iOS, and web platforms
The implementation follows the TimeSafari development principles of learning through implementation, designing for failure, and measuring everything to ensure a robust and reliable notification system.