# TimeSafari Daily Notification Plugin Integration Guide **Author**: Matthew Raymer **Version**: 2.1.0 **Created**: 2025-01-27 12:00:00 UTC **Last Updated**: 2025-10-07 04:32:12 UTC ## Overview This document provides comprehensive step-by-step instructions for integrating the TimeSafari Daily Notification Plugin into the TimeSafari application. The plugin now features a **generic polling interface** where the host app defines the inputs and response format, and the plugin provides a robust polling routine that can be used across iOS, Android, and Web platforms. ### New Generic Polling Architecture The plugin provides a **structured request/response polling system** where: 1. **Host App Defines**: Request schema, response schema, transformation logic, notification logic 2. **Plugin Provides**: Generic polling routine with retry logic, authentication, scheduling, storage pressure management 3. **Benefits**: Platform-agnostic, flexible, testable, maintainable ### TimeSafari Community Features The Daily Notification Plugin supports TimeSafari's community-building goals by providing reliable daily notifications for: **Offers** - New offers directed to me - Changed offers directed to me - New offers to my projects - Changed offers to my projects - New offers to my favorited projects - Changed offers to my favorited projects **Projects** - Local projects that are new - Local projects that have changed - Projects with content of interest that are new - Favorited projects that have changed **People** - Local people who are new - Local people who have changed - People with content of interest who are new - Favorited people who have changed - People in my contacts who have changed **Items** - Local items that are new - Local items that have changed - Favorited items that have changed All notifications are delivered through a single route that can be queried or bundled for efficient delivery while maintaining privacy-preserving communication. This plugin provides enterprise-grade daily notification functionality with dual scheduling, callback support, TTL-at-fire logic, and comprehensive observability across Web (PWA), Mobile (Capacitor), and Desktop (Electron) platforms. ## Prerequisites - Node.js 18+ and npm installed - Android Studio (for Android development) - Xcode 14+ (for iOS development) - Git access to the TimeSafari daily-notification-plugin repository - Understanding of Capacitor plugin architecture - Basic knowledge of TypeScript and Vue.js (for TimeSafari integration) - Understanding of TimeSafari's privacy-preserving claims architecture - Familiarity with decentralized identifiers (DIDs) and cryptographic verification ## Plugin Repository Structure The TimeSafari Daily Notification Plugin follows this structure: ``` daily-notification-plugin/ ├── android/ │ ├── build.gradle │ ├── src/main/java/com/timesafari/dailynotification/ │ │ ├── DailyNotificationPlugin.java │ │ ├── NotificationWorker.java │ │ ├── DatabaseManager.java │ │ └── CallbackRegistry.java │ └── src/main/AndroidManifest.xml ├── ios/ │ ├── DailyNotificationPlugin.swift │ ├── NotificationManager.swift │ ├── ContentFetcher.swift │ ├── CallbackRegistry.swift │ └── DailyNotificationPlugin.podspec ├── src/ │ ├── definitions.ts │ ├── daily-notification.ts │ ├── callback-registry.ts │ ├── observability.ts │ └── web/ │ ├── index.ts │ ├── service-worker-manager.ts │ └── sw.ts ├── dist/ │ ├── plugin.js │ ├── esm/ │ └── web/ ├── package.json ├── capacitor.config.ts └── README.md ``` ## Generic Polling Integration ### Quick Start with Generic Polling The new generic polling interface allows TimeSafari to define exactly what data it needs and how to process it: ```typescript import { GenericPollingRequest, PollingScheduleConfig, StarredProjectsRequest, StarredProjectsResponse } from '@timesafari/polling-contracts'; // 1. Define your polling request const starredProjectsRequest: GenericPollingRequest = { endpoint: '/api/v2/report/plansLastUpdatedBetween', method: 'POST', headers: { 'Content-Type': 'application/json', 'User-Agent': 'TimeSafari-DailyNotificationPlugin/1.0.0' }, body: { planIds: [], // Will be populated from user settings afterId: undefined, // Will be populated from watermark limit: 100 }, responseSchema: { validate: (data: any): data is StarredProjectsResponse => { return data && Array.isArray(data.data) && typeof data.hitLimit === 'boolean' && data.pagination && typeof data.pagination.hasMore === 'boolean'; }, transformError: (error: any) => ({ code: 'VALIDATION_ERROR', message: error.message || 'Validation failed', retryable: false }) }, retryConfig: { maxAttempts: 3, backoffStrategy: 'exponential', baseDelayMs: 1000 }, timeoutMs: 30000 }; // 2. Schedule the polling const scheduleConfig: PollingScheduleConfig = { request: starredProjectsRequest, schedule: { cronExpression: '0 10,16 * * *', // 10 AM and 4 PM daily timezone: 'UTC', maxConcurrentPolls: 1 }, notificationConfig: { enabled: true, templates: { singleUpdate: '{projectName} has been updated', multipleUpdates: 'You have {count} new updates in your starred projects' }, groupingRules: { maxGroupSize: 5, timeWindowMinutes: 5 } }, stateConfig: { watermarkKey: 'lastAckedStarredPlanChangesJwtId', storageAdapter: new TimeSafariStorageAdapter() } }; // 3. Execute the polling const scheduleId = await DailyNotification.schedulePoll(scheduleConfig); ``` ### Host App Integration Pattern ```typescript // TimeSafari app integration class TimeSafariPollingService { private pollingManager: GenericPollingManager; constructor() { this.pollingManager = new GenericPollingManager(jwtManager); } async setupStarredProjectsPolling(): Promise { // Get user's starred projects const starredProjects = await this.getUserStarredProjects(); // Update request body with user data starredProjectsRequest.body.planIds = starredProjects; // Get current watermark const watermark = await this.getCurrentWatermark(); starredProjectsRequest.body.afterId = watermark; // Schedule the poll const scheduleId = await this.pollingManager.schedulePoll(scheduleConfig); return scheduleId; } async handlePollingResult(result: PollingResult): Promise { if (result.success && result.data) { const changes = result.data.data; if (changes.length > 0) { // Generate notifications await this.generateNotifications(changes); // Update watermark with CAS const latestJwtId = changes[changes.length - 1].planSummary.jwtId; await this.updateWatermark(latestJwtId); // Acknowledge changes with server await this.acknowledgeChanges(changes.map(c => c.planSummary.jwtId)); } } else if (result.error) { console.error('Polling failed:', result.error); // Handle error (retry, notify user, etc.) } } } ``` ## Integration Steps ### 1. Install Plugin and Contracts Package Add the plugin and contracts package to your `package.json` dependencies: ```json { "dependencies": { "@timesafari/daily-notification-plugin": "git+https://github.com/timesafari/daily-notification-plugin.git#main", "@timesafari/polling-contracts": "file:./packages/polling-contracts" } } ``` Or install directly via npm: ```bash npm install git+https://github.com/timesafari/daily-notification-plugin.git#main npm install ./packages/polling-contracts ``` ### 2. Configure Capacitor Update `capacitor.config.ts` to include the plugin configuration: ```typescript import { CapacitorConfig } from '@capacitor/cli'; const config: CapacitorConfig = { appId: 'app.timesafari', appName: 'TimeSafari', webDir: 'dist', server: { cleartext: true }, plugins: { // Existing TimeSafari plugins... App: { appUrlOpen: { handlers: [ { url: 'timesafari://*', autoVerify: true } ] } }, SplashScreen: { launchShowDuration: 3000, launchAutoHide: true, backgroundColor: '#ffffff', androidSplashResourceName: 'splash', androidScaleType: 'CENTER_CROP', showSpinner: false, androidSpinnerStyle: 'large', iosSpinnerStyle: 'small', spinnerColor: '#999999', splashFullScreen: true, splashImmersive: true }, CapSQLite: { iosDatabaseLocation: 'Library/CapacitorDatabase', iosIsEncryption: false, iosBiometric: { biometricAuth: false, biometricTitle: 'Biometric login for TimeSafari' }, androidIsEncryption: false, androidBiometric: { biometricAuth: false, biometricTitle: 'Biometric login for TimeSafari' }, electronIsEncryption: false }, // Add Daily Notification Plugin configuration with generic polling support DailyNotification: { // Plugin-specific configuration defaultChannel: 'timesafari_community', enableSound: true, enableVibration: true, enableLights: true, priority: 'high', // Generic Polling Support genericPolling: { enabled: true, schedules: [ // Starred Projects Polling { request: { endpoint: '/api/v2/report/plansLastUpdatedBetween', method: 'POST', headers: { 'Content-Type': 'application/json', 'User-Agent': 'TimeSafari-DailyNotificationPlugin/1.0.0' }, body: { planIds: [], // Populated from user settings afterId: undefined, // Populated from watermark limit: 100 }, responseSchema: { validate: (data: any) => data && Array.isArray(data.data), transformError: (error: any) => ({ code: 'VALIDATION_ERROR', message: error.message, retryable: false }) }, retryConfig: { maxAttempts: 3, backoffStrategy: 'exponential', baseDelayMs: 1000 }, timeoutMs: 30000 }, schedule: { cronExpression: '0 10,16 * * *', // 10 AM and 4 PM daily timezone: 'UTC', maxConcurrentPolls: 1 }, notificationConfig: { enabled: true, templates: { singleUpdate: '{projectName} has been updated', multipleUpdates: 'You have {count} new updates in your starred projects' }, groupingRules: { maxGroupSize: 5, timeWindowMinutes: 5 } }, stateConfig: { watermarkKey: 'lastAckedStarredPlanChangesJwtId', storageAdapter: 'timesafari' // Use TimeSafari's storage } } ], maxConcurrentPolls: 3, globalRetryConfig: { maxAttempts: 3, backoffStrategy: 'exponential', baseDelayMs: 1000 } }, // Legacy dual scheduling configuration (for backward compatibility) contentFetch: { enabled: true, schedule: '0 8 * * *', // 8 AM daily - fetch community updates url: 'https://endorser.ch/api/v2/report/notifications/bundle', headers: { 'Authorization': 'Bearer your-jwt-token', 'Content-Type': 'application/json', 'X-Privacy-Level': 'user-controlled' }, ttlSeconds: 3600, timeout: 30000, retryAttempts: 3, retryDelay: 5000 }, userNotification: { enabled: true, schedule: '0 9 * * *', title: 'TimeSafari Community Update', body: 'New offers, projects, people, and items await your attention!', sound: true, vibration: true, priority: 'high' }, // Observability configuration observability: { enableLogging: true, logLevel: 'info', enableMetrics: true, enableHealthChecks: true, telemetryConfig: { lowCardinalityMetrics: true, piiRedaction: true, retentionDays: 30 } } } }, // ... rest of your config }; export default config; ``` ### 3. Android Integration #### 3.1 Update Android Settings Modify `android/settings.gradle` to include the plugin: ```gradle include ':app' include ':capacitor-cordova-android-plugins' include ':daily-notification-plugin' project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/') project(':daily-notification-plugin').projectDir = new File('../node_modules/@timesafari/daily-notification-plugin/android/') apply from: 'capacitor.settings.gradle' ``` #### 3.2 Update Android App Build Configuration Modify `android/app/build.gradle` to include the plugin dependency: ```gradle dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion" implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion" implementation project(':capacitor-android') implementation project(':capacitor-community-sqlite') implementation "androidx.biometric:biometric:1.2.0-alpha05" // Add Daily Notification Plugin implementation project(':daily-notification-plugin') // Required dependencies for the plugin implementation "androidx.room:room-runtime:2.6.1" implementation "androidx.work:work-runtime-ktx:2.9.0" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3" annotationProcessor "androidx.room:room-compiler:2.6.1" testImplementation "junit:junit:$junitVersion" androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" implementation project(':capacitor-cordova-android-plugins') } ``` #### 3.3 Update Android Manifest Add required permissions to `android/app/src/main/AndroidManifest.xml`: ```xml ``` ### 4. iOS Integration #### 4.1 Update Podfile Modify `ios/App/Podfile` to include the plugin: ```ruby require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers' platform :ios, '13.0' use_frameworks! # workaround to avoid Xcode caching of Pods that requires # Product -> Clean Build Folder after new Cordova plugins installed # Requires CocoaPods 1.6 or newer install! 'cocoapods', :disable_input_output_paths => true def capacitor_pods pod 'Capacitor', :path => '../../node_modules/@capacitor/ios' pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios' pod 'CapacitorCommunitySqlite', :path => '../../node_modules/@capacitor-community/sqlite' pod 'CapacitorMlkitBarcodeScanning', :path => '../../node_modules/@capacitor-mlkit/barcode-scanning' pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app' pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera' pod 'CapacitorClipboard', :path => '../../node_modules/@capacitor/clipboard' pod 'CapacitorFilesystem', :path => '../../node_modules/@capacitor/filesystem' pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share' pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar' pod 'CapawesomeCapacitorFilePicker', :path => '../../node_modules/@capawesome/capacitor-file-picker' # Add Daily Notification Plugin pod 'DailyNotificationPlugin', :path => '../../node_modules/@timesafari/daily-notification-plugin/ios' end target 'App' do capacitor_pods # Add your Pods here end post_install do |installer| assertDeploymentTarget(installer) installer.pods_project.targets.each do |target| target.build_configurations.each do |config| config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64' end end end ``` #### 4.2 Update iOS Info.plist Add required permissions to `ios/App/App/Info.plist`: ```xml UIBackgroundModes background-app-refresh background-processing background-fetch BGTaskSchedulerPermittedIdentifiers com.timesafari.dailynotification.content-fetch com.timesafari.dailynotification.notification-delivery NSUserNotificationsUsageDescription TimeSafari needs permission to send you notifications about important updates and reminders. NSBackgroundTasksUsageDescription TimeSafari uses background processing to fetch and deliver daily notifications. ``` #### 4.3 Enable iOS Capabilities 1. Open your project in Xcode 2. Select your app target 3. Go to "Signing & Capabilities" 4. Add the following capabilities: - **Background Modes** - Enable "Background App Refresh" - Enable "Background Processing" - **Push Notifications** (if using push notifications) ### 5. TypeScript Integration #### 5.1 Create Plugin Service Create `src/services/DailyNotificationService.ts`: ```typescript import { DailyNotification } from '@timesafari/daily-notification-plugin'; import { DualScheduleConfiguration, ContentFetchConfig, UserNotificationConfig, CallbackEvent } from '@timesafari/daily-notification-plugin'; import { GenericPollingRequest, PollingScheduleConfig, PollingResult, StarredProjectsRequest, StarredProjectsResponse, calculateBackoffDelay, createDefaultOutboxPressureManager } from '@timesafari/polling-contracts'; import { logger } from '@/utils/logger'; /** * Service for managing daily notifications in TimeSafari * Supports community building through gifts, gratitude, and collaborative projects * Provides privacy-preserving notification delivery with user-controlled visibility * * @author Matthew Raymer * @version 2.0.0 * @since 2025 */ export class DailyNotificationService { private static instance: DailyNotificationService; private isInitialized = false; private callbacks: Map = new Map(); private constructor() {} /** * Get singleton instance */ public static getInstance(): DailyNotificationService { if (!DailyNotificationService.instance) { DailyNotificationService.instance = new DailyNotificationService(); } return DailyNotificationService.instance; } /** * Initialize the daily notification service * Must be called before using any notification features */ public async initialize(): Promise { if (this.isInitialized) { logger.debug('[DailyNotificationService] Already initialized'); return; } try { // Request permissions const permissionResult = await DailyNotification.requestPermissions(); logger.debug('[DailyNotificationService] Permission result:', permissionResult); if (!permissionResult.granted) { throw new Error('Notification permissions not granted'); } // Configure the plugin for TimeSafari community features await DailyNotification.configure({ dbPath: 'timesafari_community_notifications.db', storage: 'tiered', ttlSeconds: 3600, prefetchLeadMinutes: 30, maxNotificationsPerDay: 5, retentionDays: 30 }); // Register default callbacks await this.registerDefaultCallbacks(); this.isInitialized = true; logger.debug('[DailyNotificationService] Successfully initialized'); } catch (error) { logger.error('[DailyNotificationService] Initialization failed:', error); throw error; } } /** * Schedule a basic daily notification (backward compatible) * @param options Notification options */ public async scheduleDailyNotification(options: { title: string; body: string; schedule: string; // Cron expression url?: string; actions?: Array<{ id: string; title: string }>; }): Promise { if (!this.isInitialized) { throw new Error('DailyNotificationService not initialized'); } try { await DailyNotification.scheduleDailyNotification({ title: options.title, body: options.body, time: this.cronToTime(options.schedule), url: options.url, sound: true, priority: 'high', retryCount: 3, retryInterval: 5000, offlineFallback: true }); logger.debug('[DailyNotificationService] Daily notification scheduled:', options.title); } catch (error) { logger.error('[DailyNotificationService] Failed to schedule daily notification:', error); throw error; } } /** * Schedule dual notification (content fetch + user notification) * @param config Dual scheduling configuration */ public async scheduleDualNotification(config: DualScheduleConfiguration): Promise { if (!this.isInitialized) { throw new Error('DailyNotificationService not initialized'); } try { await DailyNotification.scheduleDualNotification(config); logger.debug('[DailyNotificationService] Dual notification scheduled'); } catch (error) { logger.error('[DailyNotificationService] Failed to schedule dual notification:', error); throw error; } } /** * Schedule content fetching separately * @param config Content fetch configuration */ public async scheduleContentFetch(config: ContentFetchConfig): Promise { if (!this.isInitialized) { throw new Error('DailyNotificationService not initialized'); } try { await DailyNotification.scheduleContentFetch(config); logger.debug('[DailyNotificationService] Content fetch scheduled'); } catch (error) { logger.error('[DailyNotificationService] Failed to schedule content fetch:', error); throw error; } } /** * Schedule user notification separately * @param config User notification configuration */ public async scheduleUserNotification(config: UserNotificationConfig): Promise { if (!this.isInitialized) { throw new Error('DailyNotificationService not initialized'); } try { await DailyNotification.scheduleUserNotification(config); logger.debug('[DailyNotificationService] User notification scheduled'); } catch (error) { logger.error('[DailyNotificationService] Failed to schedule user notification:', error); throw error; } } /** * Register a callback function * @param name Callback name * @param callback Callback function */ public async registerCallback(name: string, callback: Function): Promise { if (!this.isInitialized) { throw new Error('DailyNotificationService not initialized'); } try { await DailyNotification.registerCallback(name, callback); this.callbacks.set(name, callback); logger.debug('[DailyNotificationService] Callback registered:', name); } catch (error) { logger.error('[DailyNotificationService] Failed to register callback:', error); throw error; } } /** * Unregister a callback function * @param name Callback name */ public async unregisterCallback(name: string): Promise { if (!this.isInitialized) { throw new Error('DailyNotificationService not initialized'); } try { await DailyNotification.unregisterCallback(name); this.callbacks.delete(name); logger.debug('[DailyNotificationService] Callback unregistered:', name); } catch (error) { logger.error('[DailyNotificationService] Failed to unregister callback:', error); throw error; } } /** * Get dual schedule status */ public async getDualScheduleStatus(): Promise { if (!this.isInitialized) { throw new Error('DailyNotificationService not initialized'); } try { const status = await DailyNotification.getDualScheduleStatus(); logger.debug('[DailyNotificationService] Status retrieved:', status); return status; } catch (error) { logger.error('[DailyNotificationService] Failed to get status:', error); throw error; } } /** * Cancel all notifications */ public async cancelAllNotifications(): Promise { if (!this.isInitialized) { throw new Error('DailyNotificationService not initialized'); } try { await DailyNotification.cancelDualSchedule(); logger.debug('[DailyNotificationService] All notifications cancelled'); } catch (error) { logger.error('[DailyNotificationService] Failed to cancel notifications:', error); throw error; } } /** * Get battery status and optimization info */ public async getBatteryStatus(): Promise { if (!this.isInitialized) { throw new Error('DailyNotificationService not initialized'); } try { const batteryStatus = await DailyNotification.getBatteryStatus(); logger.debug('[DailyNotificationService] Battery status:', batteryStatus); return batteryStatus; } catch (error) { logger.error('[DailyNotificationService] Failed to get battery status:', error); throw error; } } /** * Request battery optimization exemption */ public async requestBatteryOptimizationExemption(): Promise { if (!this.isInitialized) { throw new Error('DailyNotificationService not initialized'); } try { await DailyNotification.requestBatteryOptimizationExemption(); logger.debug('[DailyNotificationService] Battery optimization exemption requested'); } catch (error) { logger.error('[DailyNotificationService] Failed to request battery exemption:', error); throw error; } } /** * Register default callbacks for TimeSafari notification types */ private async registerDefaultCallbacks(): Promise { // Offers notification callback await this.registerCallback('offers', async (event: CallbackEvent) => { try { await this.handleOffersNotification(event); } catch (error) { logger.error('[DailyNotificationService] Offers callback failed:', error); } }); // Projects notification callback await this.registerCallback('projects', async (event: CallbackEvent) => { try { await this.handleProjectsNotification(event); } catch (error) { logger.error('[DailyNotificationService] Projects callback failed:', error); } }); // People notification callback await this.registerCallback('people', async (event: CallbackEvent) => { try { await this.handlePeopleNotification(event); } catch (error) { logger.error('[DailyNotificationService] People callback failed:', error); } }); // Items notification callback await this.registerCallback('items', async (event: CallbackEvent) => { try { await this.handleItemsNotification(event); } catch (error) { logger.error('[DailyNotificationService] Items callback failed:', error); } }); // Community analytics callback await this.registerCallback('communityAnalytics', async (event: CallbackEvent) => { try { // Send community events to analytics service await fetch('https://analytics.timesafari.com/community-events', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer your-analytics-token' }, body: JSON.stringify({ event: 'community_notification', data: event, timestamp: new Date().toISOString(), privacyLevel: 'aggregated' // Respect privacy-preserving architecture }) }); } catch (error) { logger.error('[DailyNotificationService] Community analytics callback failed:', error); } }); } /** * Process Endorser.ch notification bundle using parallel API requests * @param data Notification bundle data */ private async processEndorserNotificationBundle(data: any): Promise { try { const { userDid, starredPlanIds, lastKnownOfferId, lastKnownPlanId } = data; // Make parallel requests to Endorser.ch API endpoints const requests = [ // Offers to person fetch(`https://endorser.ch/api/v2/report/offers?recipientId=${userDid}&afterId=${lastKnownOfferId}`, { headers: { 'Authorization': 'Bearer your-jwt-token' } }), // Offers to user's projects fetch(`https://endorser.ch/api/v2/report/offersToPlansOwnedByMe?afterId=${lastKnownOfferId}`, { headers: { 'Authorization': 'Bearer your-jwt-token' } }), // Changes to starred projects fetch('https://endorser.ch/api/v2/report/plansLastUpdatedBetween', { method: 'POST', headers: { 'Authorization': 'Bearer your-jwt-token', 'Content-Type': 'application/json' }, body: JSON.stringify({ planIds: starredPlanIds, afterId: lastKnownPlanId }) }) ]; const [offersToPerson, offersToProjects, starredChanges] = await Promise.all(requests); const notificationData = { offersToPerson: await offersToPerson.json(), offersToProjects: await offersToProjects.json(), starredChanges: await starredChanges.json() }; // Process each notification type await this.handleOffersNotification(notificationData.offersToPerson); await this.handleProjectsNotification(notificationData.starredChanges); logger.debug('[DailyNotificationService] Processed Endorser.ch notification bundle'); } catch (error) { logger.error('[DailyNotificationService] Failed to process Endorser.ch bundle:', error); } } /** * Handle offers notification events from Endorser.ch API * @param event Callback event */ private async handleOffersNotification(event: CallbackEvent): Promise { // Handle offers notifications: new/changed offers to me, my projects, favorited projects logger.debug('[DailyNotificationService] Handling offers notification:', event); if (event.data && event.data.length > 0) { // Process OfferSummaryArrayMaybeMoreBody format event.data.forEach((offer: any) => { logger.debug('[DailyNotificationService] Processing offer:', { jwtId: offer.jwtId, handleId: offer.handleId, offeredByDid: offer.offeredByDid, recipientDid: offer.recipientDid, objectDescription: offer.objectDescription }); }); // Check if there are more offers to fetch if (event.hitLimit) { const lastOffer = event.data[event.data.length - 1]; logger.debug('[DailyNotificationService] More offers available, last JWT ID:', lastOffer.jwtId); } } } /** * Handle projects notification events from Endorser.ch API * @param event Callback event */ private async handleProjectsNotification(event: CallbackEvent): Promise { // Handle projects notifications: local new/changed, content of interest, favorited logger.debug('[DailyNotificationService] Handling projects notification:', event); if (event.data && event.data.length > 0) { // Process PlanSummaryAndPreviousClaimArrayMaybeMore format event.data.forEach((planData: any) => { const { plan, wrappedClaimBefore } = planData; logger.debug('[DailyNotificationService] Processing project change:', { jwtId: plan.jwtId, handleId: plan.handleId, name: plan.name, issuerDid: plan.issuerDid, hasPreviousClaim: !!wrappedClaimBefore }); }); // Check if there are more project changes to fetch if (event.hitLimit) { const lastPlan = event.data[event.data.length - 1]; logger.debug('[DailyNotificationService] More project changes available, last JWT ID:', lastPlan.plan.jwtId); } } } /** * Handle people notification events * @param event Callback event */ private async handlePeopleNotification(event: CallbackEvent): Promise { // Handle people notifications: local new/changed, content of interest, favorited, contacts logger.debug('[DailyNotificationService] Handling people notification:', event); // Implementation would process people data and update local state } /** * Handle items notification events * @param event Callback event */ private async handleItemsNotification(event: CallbackEvent): Promise { // Handle items notifications: local new/changed, favorited logger.debug('[DailyNotificationService] Handling items notification:', event); // Implementation would process items data and update local state } /** * Update trust network with notification events * @param event Callback event */ private async updateTrustNetwork(event: CallbackEvent): Promise { // Implement trust network update logic here // This would integrate with TimeSafari's DID-based trust system logger.debug('[DailyNotificationService] Updating trust network:', event); } /** * Handle privacy-preserving notification delivery * @param event Callback event */ private async handlePrivacyPreservingNotification(event: CallbackEvent): Promise { // Implement privacy-preserving notification logic here // This would respect user-controlled visibility settings logger.debug('[DailyNotificationService] Handling privacy-preserving notification:', event); } /** * Save notification event to database * @param event Callback event */ private async saveToDatabase(event: CallbackEvent): Promise { // Implement your database save logic here logger.debug('[DailyNotificationService] Saving to database:', event); } /** * Convert cron expression to time string * @param cron Cron expression (e.g., "0 9 * * *") */ private cronToTime(cron: string): string { const parts = cron.split(' '); if (parts.length >= 2) { const hour = parts[1].padStart(2, '0'); const minute = parts[0].padStart(2, '0'); return `${hour}:${minute}`; } return '09:00'; // Default to 9 AM } /** * Check if the service is initialized */ public isServiceInitialized(): boolean { return this.isInitialized; } /** * Get service version */ public getVersion(): string { return '2.0.0'; } /** * Setup generic polling for starred projects * @param starredProjectIds Array of starred project IDs * @param currentWatermark Current watermark JWT ID */ public async setupStarredProjectsPolling( starredProjectIds: string[], currentWatermark?: string ): Promise { if (!this.isInitialized) { throw new Error('DailyNotificationService not initialized'); } try { // Create the polling request const starredProjectsRequest: GenericPollingRequest = { endpoint: '/api/v2/report/plansLastUpdatedBetween', method: 'POST', headers: { 'Content-Type': 'application/json', 'User-Agent': 'TimeSafari-DailyNotificationPlugin/1.0.0', 'Authorization': `Bearer ${await this.getJwtToken()}` }, body: { planIds: starredProjectIds, afterId: currentWatermark, limit: 100 }, responseSchema: { validate: (data: any): data is StarredProjectsResponse => { return data && Array.isArray(data.data) && typeof data.hitLimit === 'boolean' && data.pagination && typeof data.pagination.hasMore === 'boolean'; }, transformError: (error: any) => ({ code: 'VALIDATION_ERROR', message: error.message || 'Validation failed', retryable: false }) }, retryConfig: { maxAttempts: 3, backoffStrategy: 'exponential', baseDelayMs: 1000 }, timeoutMs: 30000 }; // Create the schedule configuration const scheduleConfig: PollingScheduleConfig = { request: starredProjectsRequest, schedule: { cronExpression: '0 10,16 * * *', // 10 AM and 4 PM daily timezone: 'UTC', maxConcurrentPolls: 1 }, notificationConfig: { enabled: true, templates: { singleUpdate: '{projectName} has been updated', multipleUpdates: 'You have {count} new updates in your starred projects' }, groupingRules: { maxGroupSize: 5, timeWindowMinutes: 5 } }, stateConfig: { watermarkKey: 'lastAckedStarredPlanChangesJwtId', storageAdapter: 'timesafari' } }; // Schedule the polling const scheduleId = await DailyNotification.schedulePoll(scheduleConfig); logger.debug('[DailyNotificationService] Starred projects polling scheduled:', scheduleId); return scheduleId; } catch (error) { logger.error('[DailyNotificationService] Failed to setup starred projects polling:', error); throw error; } } /** * Handle polling results * @param result Polling result */ public async handlePollingResult(result: PollingResult): Promise { if (!this.isInitialized) { throw new Error('DailyNotificationService not initialized'); } try { if (result.success && result.data) { const changes = result.data.data; if (changes.length > 0) { // Generate notifications await this.generateNotifications(changes); // Update watermark with CAS const latestJwtId = changes[changes.length - 1].planSummary.jwtId; await this.updateWatermark(latestJwtId); // Acknowledge changes with server await this.acknowledgeChanges(changes.map(c => c.planSummary.jwtId)); logger.debug('[DailyNotificationService] Processed polling result:', { changeCount: changes.length, latestJwtId }); } } else if (result.error) { logger.error('[DailyNotificationService] Polling failed:', result.error); // Handle error (retry, notify user, etc.) await this.handlePollingError(result.error); } } catch (error) { logger.error('[DailyNotificationService] Failed to handle polling result:', error); throw error; } } /** * Get JWT token for authentication */ private async getJwtToken(): Promise { // Implementation would get JWT token from TimeSafari's auth system return 'your-jwt-token'; } /** * Generate notifications from polling results */ private async generateNotifications(changes: any[]): Promise { // Implementation would generate notifications based on changes logger.debug('[DailyNotificationService] Generating notifications for changes:', changes.length); } /** * Update watermark with compare-and-swap */ private async updateWatermark(jwtId: string): Promise { // Implementation would update watermark using CAS logger.debug('[DailyNotificationService] Updating watermark:', jwtId); } /** * Acknowledge changes with server */ private async acknowledgeChanges(jwtIds: string[]): Promise { // Implementation would acknowledge changes with server logger.debug('[DailyNotificationService] Acknowledging changes:', jwtIds.length); } /** * Handle polling errors */ private async handlePollingError(error: any): Promise { // Implementation would handle polling errors logger.error('[DailyNotificationService] Handling polling error:', error); } } ``` #### 5.2 Add to PlatformServiceMixin Update `src/utils/PlatformServiceMixin.ts` to include notification methods: ```typescript import { DailyNotificationService } from '@/services/DailyNotificationService'; // Add to the mixin object export const PlatformServiceMixin = { // ... existing methods /** * Schedule a daily notification * @param options Notification options */ async $scheduleDailyNotification(options: { title: string; body: string; schedule: string; url?: string; actions?: Array<{ id: string; title: string }>; }): Promise { const notificationService = DailyNotificationService.getInstance(); return await notificationService.scheduleDailyNotification(options); }, /** * Schedule dual notification (content fetch + user notification) * @param config Dual scheduling configuration */ async $scheduleDualNotification(config: any): Promise { const notificationService = DailyNotificationService.getInstance(); return await notificationService.scheduleDualNotification(config); }, /** * Register a notification callback * @param name Callback name * @param callback Callback function */ async $registerNotificationCallback(name: string, callback: Function): Promise { const notificationService = DailyNotificationService.getInstance(); return await notificationService.registerCallback(name, callback); }, /** * Get notification status */ async $getNotificationStatus(): Promise { const notificationService = DailyNotificationService.getInstance(); return await notificationService.getDualScheduleStatus(); }, /** * Cancel all notifications */ async $cancelAllNotifications(): Promise { const notificationService = DailyNotificationService.getInstance(); return await notificationService.cancelAllNotifications(); }, /** * Get battery status */ async $getBatteryStatus(): Promise { const notificationService = DailyNotificationService.getInstance(); return await notificationService.getBatteryStatus(); }, /** * Request battery optimization exemption */ async $requestBatteryOptimizationExemption(): Promise { const notificationService = DailyNotificationService.getInstance(); return await notificationService.requestBatteryOptimizationExemption(); }, /** * Setup generic polling for starred projects * @param starredProjectIds Array of starred project IDs * @param currentWatermark Current watermark JWT ID */ async $setupStarredProjectsPolling(starredProjectIds: string[], currentWatermark?: string): Promise { const notificationService = DailyNotificationService.getInstance(); return await notificationService.setupStarredProjectsPolling(starredProjectIds, currentWatermark); }, /** * Handle polling results * @param result Polling result */ async $handlePollingResult(result: any): Promise { const notificationService = DailyNotificationService.getInstance(); return await notificationService.handlePollingResult(result); }, // ... rest of existing methods }; ``` #### 5.3 Update TypeScript Declarations Add to the Vue module declaration in `src/utils/PlatformServiceMixin.ts`: ```typescript declare module "@vue/runtime-core" { interface ComponentCustomProperties { // ... existing methods // Daily Notification methods $scheduleDailyNotification(options: { title: string; body: string; schedule: string; url?: string; actions?: Array<{ id: string; title: string }>; }): Promise; $scheduleDualNotification(config: any): Promise; $registerNotificationCallback(name: string, callback: Function): Promise; $getNotificationStatus(): Promise; $cancelAllNotifications(): Promise; $getBatteryStatus(): Promise; $requestBatteryOptimizationExemption(): Promise; $setupStarredProjectsPolling(starredProjectIds: string[], currentWatermark?: string): Promise; $handlePollingResult(result: any): Promise; } } ``` ### 6. Initialization in App #### 6.1 Initialize in Main App Component Update your main app component (e.g., `src/App.vue` or `src/main.ts`) to initialize the notification service: ```typescript import { DailyNotificationService } from '@/services/DailyNotificationService'; // In your app initialization async function initializeApp() { try { // Initialize other services first await initializeDatabase(); await initializePlatformService(); // Initialize daily notifications const notificationService = DailyNotificationService.getInstance(); await notificationService.initialize(); logger.debug('[App] All services initialized successfully'); } catch (error) { logger.error('[App] Failed to initialize services:', error); // Handle initialization error } } ``` #### 6.2 Initialize in Platform Service Alternatively, initialize in your platform service startup: ```typescript // In src/services/platforms/CapacitorPlatformService.ts or WebPlatformService.ts import { DailyNotificationService } from '@/services/DailyNotificationService'; export class CapacitorPlatformService implements PlatformService { // ... existing methods private async initializeDatabase(): Promise { // ... existing database initialization // Initialize daily notifications after database is ready try { const notificationService = DailyNotificationService.getInstance(); await notificationService.initialize(); logger.debug('[CapacitorPlatformService] Daily notifications initialized'); } catch (error) { logger.warn('[CapacitorPlatformService] Failed to initialize daily notifications:', error); // Don't fail the entire initialization for notification errors } } } ``` ### 7. Usage Examples #### 7.1 Generic Polling for Starred Projects ```typescript // In a Vue component export default { data() { return { starredProjects: [], currentWatermark: null, pollingScheduleId: null }; }, async mounted() { await this.initializePolling(); }, methods: { async initializePolling() { try { // Get user's starred projects this.starredProjects = await this.getUserStarredProjects(); // Get current watermark this.currentWatermark = await this.getCurrentWatermark(); // Setup polling this.pollingScheduleId = await this.$setupStarredProjectsPolling( this.starredProjects, this.currentWatermark ); this.$notify('Starred projects polling initialized successfully'); } catch (error) { this.$notify('Failed to initialize polling: ' + error.message); } }, async getUserStarredProjects() { // Implementation would get starred projects from TimeSafari's database return ['project-1', 'project-2', 'project-3']; }, async getCurrentWatermark() { // Implementation would get current watermark from storage return '1704067200_abc123_12345678'; }, async handlePollingResult(result) { try { await this.$handlePollingResult(result); if (result.success && result.data && result.data.data.length > 0) { this.$notify(`Received ${result.data.data.length} project updates`); } } catch (error) { this.$notify('Failed to handle polling result: ' + error.message); } } } }; ``` #### 7.2 Community Update Notification ```typescript // In a Vue component export default { methods: { async scheduleCommunityUpdate() { try { await this.$scheduleDailyNotification({ title: 'TimeSafari Community Update', body: 'New offers, projects, people, and items await your attention!', schedule: '0 9 * * *', // 9 AM daily url: 'https://timesafari.com/notifications/bundle', actions: [ { id: 'view_offers', title: 'View Offers' }, { id: 'view_projects', title: 'See Projects' }, { id: 'view_people', title: 'Check People' }, { id: 'view_items', title: 'Browse Items' }, { id: 'dismiss', title: 'Dismiss' } ] }); this.$notify('Community update notification scheduled successfully'); } catch (error) { this.$notify('Failed to schedule community update: ' + error.message); } } } }; ``` #### 7.2 Community Content Fetch + Notification ```typescript async scheduleCommunityContentFetch() { try { const config = { contentFetch: { enabled: true, schedule: '0 8 * * *', // Fetch community content at 8 AM url: 'https://endorser.ch/api/v2/report/notifications/bundle', // Single route for all notification types headers: { 'Authorization': 'Bearer your-jwt-token', 'Content-Type': 'application/json', 'X-Privacy-Level': 'user-controlled' }, ttlSeconds: 3600, // 1 hour TTL for community data timeout: 30000, retryAttempts: 3, retryDelay: 5000, callbacks: { onSuccess: async (data) => { console.log('Community notifications fetched successfully:', data); // Process bundled notifications using Endorser.ch API patterns await this.processEndorserNotificationBundle(data); }, onError: async (error) => { console.error('Community content fetch failed:', error); } } }, userNotification: { enabled: true, schedule: '0 9 * * *', // Notify at 9 AM title: 'TimeSafari Community Update Ready', body: 'New offers, projects, people, and items are available!', sound: true, vibration: true, priority: 'high', actions: [ { id: 'view_offers', title: 'View Offers' }, { id: 'view_projects', title: 'See Projects' }, { id: 'view_people', title: 'Check People' }, { id: 'view_items', title: 'Browse Items' }, { id: 'dismiss', title: 'Dismiss' } ] }, relationship: { autoLink: true, contentTimeout: 300000, // 5 minutes fallbackBehavior: 'show_default' } }; await this.$scheduleDualNotification(config); this.$notify('Community content fetch scheduled successfully'); } catch (error) { this.$notify('Failed to schedule community content fetch: ' + error.message); } } ``` #### 7.3 Endorser.ch API Integration ```typescript async integrateWithEndorserAPI() { try { // Register offers callback using Endorser.ch API endpoints await this.$registerNotificationCallback('offers', async (event) => { try { // Handle offers notifications using Endorser.ch API patterns const { userDid, lastKnownOfferId } = event; // Fetch offers to person const offersToPerson = await fetch(`https://endorser.ch/api/v2/report/offers?recipientId=${userDid}&afterId=${lastKnownOfferId}`, { headers: { 'Authorization': 'Bearer your-jwt-token' } }); // Fetch offers to user's projects const offersToProjects = await fetch(`https://endorser.ch/api/v2/report/offersToPlansOwnedByMe?afterId=${lastKnownOfferId}`, { headers: { 'Authorization': 'Bearer your-jwt-token' } }); const [offersToPersonData, offersToProjectsData] = await Promise.all([ offersToPerson.json(), offersToProjects.json() ]); // Process OfferSummaryArrayMaybeMoreBody format const allOffers = [...offersToPersonData.data, ...offersToProjectsData.data]; console.log('Processed offers:', allOffers.map(offer => ({ jwtId: offer.jwtId, handleId: offer.handleId, offeredByDid: offer.offeredByDid, objectDescription: offer.objectDescription }))); } catch (error) { console.error('Offers callback failed:', error); } }); // Register projects callback using Endorser.ch API endpoints await this.$registerNotificationCallback('projects', async (event) => { try { // Handle projects notifications using Endorser.ch API patterns const { starredPlanIds, lastKnownPlanId } = event; // Fetch changes to starred projects const starredChanges = await fetch('https://endorser.ch/api/v2/report/plansLastUpdatedBetween', { method: 'POST', headers: { 'Authorization': 'Bearer your-jwt-token', 'Content-Type': 'application/json' }, body: JSON.stringify({ planIds: starredPlanIds, afterId: lastKnownPlanId }) }); const starredChangesData = await starredChanges.json(); // Process PlanSummaryAndPreviousClaimArrayMaybeMore format console.log('Processed project changes:', starredChangesData.data.map(planData => ({ jwtId: planData.plan.jwtId, handleId: planData.plan.handleId, name: planData.plan.name, issuerDid: planData.plan.issuerDid, hasPreviousClaim: !!planData.wrappedClaimBefore }))); } catch (error) { console.error('Projects callback failed:', error); } }); this.$notify('Endorser.ch API integration registered successfully'); } catch (error) { this.$notify('Failed to register Endorser.ch API integration: ' + error.message); } } ``` #### 7.4 Battery Optimization Management ```typescript async checkBatteryOptimization() { try { const batteryStatus = await this.$getBatteryStatus(); if (!batteryStatus.isOptimizationExempt) { // Request exemption from battery optimization await this.$requestBatteryOptimizationExemption(); this.$notify('Battery optimization exemption requested'); } else { this.$notify('App is already exempt from battery optimization'); } } catch (error) { this.$notify('Failed to check battery optimization: ' + error.message); } } ``` ### 8. Endorser.ch API Integration Patterns The TimeSafari Daily Notification Plugin integrates with the Endorser.ch API to fetch community activity using pagination-based filtering. The API provides several endpoints for retrieving "new" or recent activity using `afterId` and `beforeId` parameters. #### 8.1 Core Pagination Pattern All Endorser.ch "new" activity endpoints use the same pagination pattern: - **`afterId`**: JWT ID of the entry after which to look (exclusive) - gets newer entries - **`beforeId`**: JWT ID of the entry before which to look (exclusive) - gets older entries - **Results**: Returned in reverse chronological order (newest first) - **Response Format**: `{ data: [...], hitLimit: boolean }` #### 8.2 Key Endpoints **Offers to Person** ```typescript GET /api/v2/report/offers?recipientId=${userDid}&afterId=${lastKnownOfferId} ``` **Offers to User's Projects** ```typescript GET /api/v2/report/offersToPlansOwnedByMe?afterId=${lastKnownOfferId} ``` **Changes to Starred Projects** ```typescript POST /api/v2/report/plansLastUpdatedBetween { "planIds": ["plan-123", "plan-456"], "afterId": "01HSE3R9MAC0FT3P3KZ382TWV7" } ``` #### 8.3 Parallel Requests Implementation ```typescript async function getNewActivity(userDid, starredPlanIds, lastKnownOfferId, lastKnownPlanId) { const requests = [ // Offers to person fetch(`https://endorser.ch/api/v2/report/offers?recipientId=${userDid}&afterId=${lastKnownOfferId}`, { headers: { 'Authorization': 'Bearer your-jwt-token' } }), // Offers to user's projects fetch(`https://endorser.ch/api/v2/report/offersToPlansOwnedByMe?afterId=${lastKnownOfferId}`, { headers: { 'Authorization': 'Bearer your-jwt-token' } }), // Changes to starred projects fetch('https://endorser.ch/api/v2/report/plansLastUpdatedBetween', { method: 'POST', headers: { 'Authorization': 'Bearer your-jwt-token', 'Content-Type': 'application/json' }, body: JSON.stringify({ planIds: starredPlanIds, afterId: lastKnownPlanId }) }) ]; const [offersToPerson, offersToProjects, starredChanges] = await Promise.all(requests); return { offersToPerson: await offersToPerson.json(), offersToProjects: await offersToProjects.json(), starredChanges: await starredChanges.json() }; } ``` #### 8.4 Pagination Handling ```typescript function handlePagination(response) { if (response.hitLimit) { // There may be more data - use the last item's jwtId as afterId for next request const lastItem = response.data[response.data.length - 1]; return { hasMore: true, nextAfterId: lastItem.jwtId }; } return { hasMore: false }; } ``` ### 9. Build and Sync After making all changes, run the following commands: ```bash # Install dependencies npm install # Build the web app npm run build:capacitor # Sync with native platforms npx cap sync # For iOS, update pods cd ios/App && pod install && cd ../.. # For Android, clean and rebuild cd android && ./gradlew clean && cd .. ``` ### 10. Testing #### 10.1 Test on Android ```bash # Build and run on Android npm run build:android npx cap run android ``` #### 10.2 Test on iOS ```bash # Build and run on iOS npm run build:ios npx cap run ios ``` #### 10.3 Test on Web ```bash # Build and run on web npm run build:web npm run serve:web ``` ### 11. Troubleshooting #### 11.1 Common Issues 1. **Plugin not found**: Ensure the plugin is properly installed and the path is correct 2. **Permissions denied**: Check that all required permissions are added to manifests 3. **Build errors**: Clean and rebuild the project after adding the plugin 4. **TypeScript errors**: Ensure the plugin exports proper TypeScript definitions 5. **Background tasks not running**: Check battery optimization settings and background app refresh 6. **Endorser.ch API errors**: Verify JWT token authentication and endpoint availability #### 11.2 Debug Steps 1. Check console logs for initialization errors 2. Verify plugin is loaded in `capacitor.plugins.json` 3. Test permissions manually in device settings 4. Use browser dev tools for web platform testing 5. Check WorkManager logs on Android 6. Check BGTaskScheduler logs on iOS 7. Verify Endorser.ch API responses and pagination handling #### 11.3 Platform-Specific Issues **Android:** - Ensure WorkManager is properly configured - Check battery optimization settings - Verify exact alarm permissions - Check Room database initialization **iOS:** - Verify background modes are enabled - Check BGTaskScheduler identifiers - Ensure Core Data model is compatible - Verify notification permissions **Web:** - Ensure Service Worker is registered - Check HTTPS requirements - Verify IndexedDB compatibility - Check push notification setup **Endorser.ch API:** - Verify JWT token authentication - Check pagination parameters (afterId, beforeId) - Monitor rate limiting and hitLimit responses - Ensure proper error handling for API failures ### 12. Security Considerations - Ensure notification data doesn't contain sensitive personal information - Validate all notification inputs and callback URLs - Implement proper error handling and logging - Respect user privacy preferences and visibility settings - Follow platform-specific notification guidelines - Use HTTPS for all network operations - Implement proper authentication for callbacks - Respect TimeSafari's privacy-preserving claims architecture - Ensure user-controlled visibility for all notification data - Use cryptographic verification for sensitive notification content ### 13. Performance Considerations - Limit the number of scheduled notifications - Clean up old notifications regularly - Use efficient notification IDs - Consider battery impact on mobile devices - Implement proper caching strategies - Use circuit breaker patterns for callbacks - Monitor memory usage and database performance - Implement efficient Endorser.ch API pagination handling - Cache JWT tokens and API responses appropriately - Monitor API rate limits and implement backoff strategies ### 14. Enterprise Integration Examples #### 14.1 Community Analytics Integration ```typescript // Register community analytics callback await this.$registerNotificationCallback('communityAnalytics', async (event) => { try { // Send community events to analytics service await fetch('https://analytics.timesafari.com/community-events', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Privacy-Level': 'aggregated' }, body: JSON.stringify({ client_id: 'your-client-id', events: [{ name: 'community_notification', params: { notification_id: event.id, action: event.action, timestamp: event.timestamp, community_type: event.communityType, privacy_level: 'aggregated' } }] }) }); } catch (error) { console.error('Community analytics callback failed:', error); } }); ``` #### 14.2 Trust Network Integration ```typescript // Register trust network callback await this.$registerNotificationCallback('trustNetwork', async (event) => { try { await fetch('https://api.timesafari.com/trust-network/events', { method: 'POST', headers: { 'Authorization': 'Bearer your-trust-token', 'Content-Type': 'application/json', 'X-Privacy-Level': 'user-controlled' }, body: JSON.stringify({ Name: event.id, Action__c: event.action, Timestamp__c: new Date(event.timestamp).toISOString(), UserDid__c: event.userDid, TrustLevel__c: event.trustLevel, Data__c: JSON.stringify(event.data) }) }); } catch (error) { console.error('Trust network callback failed:', error); } }); ``` ## Conclusion This guide provides a comprehensive approach to integrating the TimeSafari Daily Notification Plugin into the TimeSafari application. The integration supports TimeSafari's core mission of fostering community building through gifts, gratitude, and collaborative projects. The plugin offers advanced features specifically designed for community engagement: - **Dual Scheduling**: Separate content fetch and user notification scheduling for community updates - **TTL-at-Fire Logic**: Content validity checking at notification time for community data - **Circuit Breaker Pattern**: Automatic failure detection and recovery for community services - **Privacy-Preserving Architecture**: Respects TimeSafari's user-controlled visibility and DID-based identity system - **Trust Network Integration**: Supports building and maintaining trust networks through notifications - **Comprehensive Observability**: Structured logging and health monitoring for community features The integration follows TimeSafari's development principles: - **Platform Services**: Uses abstracted platform services via interfaces - **Type Safety**: Implements strict TypeScript with type guards - **Modern Architecture**: Follows current platform service patterns - **Privacy-First**: Respects privacy-preserving claims architecture - **Community-Focused**: Supports community building and trust network development For questions or issues, refer to the plugin's documentation or contact the TimeSafari development team. --- **Version**: 2.1.0 **Last Updated**: 2025-10-07 04:32:12 UTC **Status**: Production Ready **Author**: Matthew Raymer