diff --git a/INTEGRATION_GUIDE.md b/INTEGRATION_GUIDE.md new file mode 100644 index 0000000..8cc9619 --- /dev/null +++ b/INTEGRATION_GUIDE.md @@ -0,0 +1,1612 @@ +# TimeSafari Daily Notification Plugin Integration Guide + +**Author**: Matthew Raymer +**Version**: 2.0.0 +**Created**: 2025-01-27 12:00:00 UTC +**Last Updated**: 2025-01-27 12:00:00 UTC + +## Overview + +This document provides comprehensive step-by-step instructions for integrating the TimeSafari Daily Notification Plugin into the TimeSafari application. TimeSafari is designed to foster community building through gifts, gratitude, and collaborative projects, making it easy for users to recognize contributions, build trust networks, and organize collective action. + +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 +``` + +## Integration Steps + +### 1. Install Plugin from Git Repository + +Add the plugin to your `package.json` dependencies: + +```json +{ + "dependencies": { + "@timesafari/daily-notification-plugin": "git+https://github.com/timesafari/daily-notification-plugin.git#main" + } +} +``` + +Or install directly via npm: +```bash +npm install git+https://github.com/timesafari/daily-notification-plugin.git#main +``` + +### 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 for TimeSafari community features + DailyNotification: { + // Plugin-specific configuration + defaultChannel: 'timesafari_community', + enableSound: true, + enableVibration: true, + enableLights: true, + priority: 'high', + // Dual scheduling configuration for community updates + contentFetch: { + enabled: true, + schedule: '0 8 * * *', // 8 AM daily - fetch community updates + 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, // 30 second timeout + retryAttempts: 3, + retryDelay: 5000 + }, + userNotification: { + enabled: true, + schedule: '0 9 * * *', // 9 AM daily - notify users of community updates + title: 'TimeSafari Community Update', + body: 'New offers, projects, people, and items await your attention!', + sound: true, + vibration: true, + priority: 'high' + }, + // Callback configuration for community features + callbacks: { + offers: { + enabled: true, + localHandler: 'handleOffersNotification' + }, + projects: { + enabled: true, + localHandler: 'handleProjectsNotification' + }, + people: { + enabled: true, + localHandler: 'handlePeopleNotification' + }, + items: { + enabled: true, + localHandler: 'handleItemsNotification' + }, + communityAnalytics: { + enabled: true, + endpoint: 'https://analytics.timesafari.com/community-events', + headers: { + 'Authorization': 'Bearer your-analytics-token', + 'Content-Type': 'application/json' + } + } + }, + // Observability configuration + observability: { + enableLogging: true, + logLevel: 'debug', + enableMetrics: true, + enableHealthChecks: true + } + } + }, + // ... 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 { 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'; + } +} +``` + +#### 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(); + }, + + // ... 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; + } +} +``` + +### 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 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.0.0 +**Last Updated**: 2025-01-27 12:00:00 UTC +**Status**: Production Ready +**Author**: Matthew Raymer diff --git a/test-apps/README.md b/test-apps/README.md index 4b84471..800d9f6 100644 --- a/test-apps/README.md +++ b/test-apps/README.md @@ -1,8 +1,8 @@ -# Test Apps Setup Guide +# TimeSafari Test Apps Setup Guide ## Overview -This guide creates minimal Capacitor test apps for validating the Daily Notification Plugin across all target platforms. +This guide creates minimal Capacitor test apps for validating the TimeSafari Daily Notification Plugin integration across all target platforms. The test apps demonstrate TimeSafari's community-building features, Endorser.ch API integration, and notification patterns. ## Directory Structure @@ -11,7 +11,11 @@ test-apps/ ├── android-test/ # Android test app ├── ios-test/ # iOS test app ├── electron-test/ # Electron test app -├── test-api/ # Test API server +├── test-api/ # TimeSafari Test API server +├── shared/ # Shared configuration and utilities +│ └── config-loader.ts # Configuration loader and mock services +├── config/ # Configuration files +│ └── timesafari-config.json ├── setup-android.sh # Android setup script ├── setup-ios.sh # iOS setup script ├── setup-electron.sh # Electron setup script @@ -27,6 +31,8 @@ test-apps/ - Android Studio (for Android) - Xcode (for iOS) - Platform-specific SDKs +- Understanding of TimeSafari's community-building purpose +- Familiarity with Endorser.ch API patterns ## Quick Start @@ -61,8 +67,9 @@ See [Enhanced Setup Guide](SETUP_GUIDE.md) for detailed manual setup instruction ## Test App Features Each test app includes: -- **Plugin Configuration**: Test shared SQLite, TTL, prefetch settings -- **Notification Scheduling**: Basic daily notification setup +- **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 - **Platform-Specific Features**: - Android: Exact alarm permissions, reboot recovery - iOS: Rolling window management, BGTaskScheduler @@ -71,13 +78,13 @@ Each test app includes: - **Error Handling**: Comprehensive error testing - **Debug Information**: Platform-specific debug data -## Test API Server +## TimeSafari Test API Server -A mock REST API server (`test-api/`) provides endpoints for testing the plugin's network functionality: +A comprehensive REST API server (`test-api/`) simulates Endorser.ch API endpoints for testing the plugin's TimeSafari-specific functionality: ### Quick Start ```bash -# Start the test API server +# Start the TimeSafari Test API server cd test-apps/test-api npm install npm start @@ -87,19 +94,30 @@ npm run demo ``` ### Key Features -- **Content Endpoints**: Generate mock notification content -- **ETag Support**: Full HTTP caching with conditional requests +- **Endorser.ch API Simulation**: Mock endpoints for offers, projects, and pagination +- **TimeSafari Notification Bundle**: Single route for bundled notifications +- **Community Analytics**: Analytics endpoint for community events +- **Pagination Support**: Full afterId/beforeId pagination testing +- **ETag Support**: HTTP caching with conditional requests - **Error Simulation**: Test various error scenarios - **Metrics**: Monitor API usage and performance - **CORS Enabled**: Cross-origin requests supported ### API Endpoints + +#### Endorser.ch API Endpoints +- `GET /api/v2/report/offers` - Get offers to person +- `GET /api/v2/report/offersToPlansOwnedByMe` - Get offers to user's projects +- `POST /api/v2/report/plansLastUpdatedBetween` - Get changes to starred projects + +#### TimeSafari API Endpoints +- `GET /api/v2/report/notifications/bundle` - Get bundled notifications +- `POST /api/analytics/community-events` - Send community analytics + +#### Legacy Endpoints - `GET /health` - Health check - `GET /api/content/:slotId` - Get notification content -- `GET /api/error/:type` - Simulate errors - `GET /api/metrics` - API metrics -- `PUT /api/content/:slotId` - Update content -- `DELETE /api/content` - Clear all content ### Platform-Specific URLs - **Web/Electron**: `http://localhost:3001` @@ -110,19 +128,25 @@ npm run demo ## Platform-Specific Testing ### Android Test App +- **TimeSafari Configuration**: Test community notification settings +- **Endorser.ch API Integration**: Test parallel API requests - **Exact Alarm Status**: Check permission and capability - **Permission Requests**: Test exact alarm permission flow - **Performance Metrics**: Monitor Android-specific optimizations - **Reboot Recovery**: Validate system restart handling ### iOS Test App +- **TimeSafari Configuration**: Test iOS community features - **Rolling Window**: Test notification limit management +- **Endorser.ch API Integration**: Test pagination patterns - **Background Tasks**: Validate BGTaskScheduler integration - **Performance Metrics**: Monitor iOS-specific optimizations - **Memory Management**: Test object pooling and cleanup ### Electron Test App +- **TimeSafari Configuration**: Test Electron community features - **Mock Implementations**: Test web platform compatibility +- **Endorser.ch API Integration**: Test API patterns - **IPC Communication**: Validate Electron-specific APIs - **Development Workflow**: Test plugin integration - **Debug Information**: Platform-specific status display @@ -155,8 +179,9 @@ npm run dev # Run in development mode ## Testing Checklist ### Core Functionality -- [ ] Plugin configuration works -- [ ] Notification scheduling succeeds +- [ ] TimeSafari configuration works +- [ ] Community notification scheduling succeeds +- [ ] Endorser.ch API integration functions properly - [ ] Error handling functions properly - [ ] Performance metrics are accurate @@ -166,9 +191,11 @@ npm run dev # Run in development mode - [ ] Electron mock implementations - [ ] Cross-platform API consistency -### Integration +### TimeSafari Integration - [ ] Plugin loads without errors - [ ] Configuration persists across sessions +- [ ] Endorser.ch API pagination works +- [ ] Community notification types process correctly - [ ] Performance optimizations active - [ ] Debug information accessible @@ -179,6 +206,7 @@ npm run dev # Run in development mode 2. **"android platform has not been added yet"** → Run `npx cap add android` first 3. **Build failures** → Check Node.js version (18+) and clear cache: `npm cache clean --force` 4. **Platform errors** → Verify platform-specific SDKs are installed +5. **API connection errors** → Ensure test API server is running on port 3001 ### Quick Fixes ```bash @@ -193,6 +221,9 @@ npx cap clean # Re-sync platforms npx cap sync + +# Restart test API server +cd test-api && npm start ``` ### Detailed Help @@ -201,7 +232,9 @@ See [Enhanced Setup Guide](SETUP_GUIDE.md) for comprehensive troubleshooting and ## Next Steps 1. **Run Setup Scripts**: Execute platform-specific setup -2. **Test Core Features**: Validate basic functionality -3. **Test Platform Features**: Verify platform-specific capabilities -4. **Integration Testing**: Test with actual plugin implementation -5. **Performance Validation**: Monitor metrics and optimizations +2. **Start Test API Server**: Run the TimeSafari Test API server +3. **Test Core Features**: Validate basic TimeSafari functionality +4. **Test Platform Features**: Verify platform-specific capabilities +5. **Test Endorser.ch Integration**: Validate API patterns and pagination +6. **Integration Testing**: Test with actual plugin implementation +7. **Performance Validation**: Monitor metrics and optimizations diff --git a/test-apps/android-test/src/index.html b/test-apps/android-test/src/index.html index 1a66c26..cc24005 100644 --- a/test-apps/android-test/src/index.html +++ b/test-apps/android-test/src/index.html @@ -89,15 +89,16 @@
-

📱 Daily Notification Plugin - Android Test

+

📱 TimeSafari Daily Notification - Android Test

Ready
- - - - + + + + +
diff --git a/test-apps/android-test/src/index.ts b/test-apps/android-test/src/index.ts index 5c6c670..3347056 100644 --- a/test-apps/android-test/src/index.ts +++ b/test-apps/android-test/src/index.ts @@ -1,77 +1,47 @@ import { Capacitor } from '@capacitor/core'; +import { ConfigLoader, MockDailyNotificationService, TestLogger } from '../shared/config-loader'; -// Mock plugin for development -const DailyNotification = { - async configure(options: any) { - console.log('Configure called:', options); - return Promise.resolve(); - }, - async scheduleDailyNotification(options: any) { - console.log('Schedule called:', options); - return Promise.resolve(); - }, - async getExactAlarmStatus() { - return Promise.resolve({ - supported: true, - enabled: false, - canSchedule: false, - fallbackWindow: '±10 minutes' - }); - }, - async requestExactAlarmPermission() { - console.log('Request exact alarm permission'); - return Promise.resolve(); - }, - async getPerformanceMetrics() { - return Promise.resolve({ - overallScore: 85, - databasePerformance: 90, - memoryEfficiency: 80, - batteryEfficiency: 85, - objectPoolEfficiency: 90, - totalDatabaseQueries: 150, - averageMemoryUsage: 25.5, - objectPoolHits: 45, - backgroundCpuUsage: 2.3, - totalNetworkRequests: 12, - recommendations: ['Enable ETag support', 'Optimize memory usage'] - }); - } -}; - -// Test interface -class TestApp { +// Test interface for TimeSafari Android integration +class TimeSafariAndroidTestApp { private statusElement: HTMLElement; private logElement: HTMLElement; + private configLoader: ConfigLoader; + private notificationService: MockDailyNotificationService; + private logger: TestLogger; constructor() { this.statusElement = document.getElementById('status')!; this.logElement = document.getElementById('log')!; + this.configLoader = ConfigLoader.getInstance(); + this.logger = new TestLogger('debug'); + this.notificationService = new MockDailyNotificationService(this.configLoader.getConfig()); this.setupEventListeners(); - this.log('Test app initialized'); + this.log('TimeSafari Android Test app initialized'); } private setupEventListeners() { document.getElementById('configure')?.addEventListener('click', () => this.testConfigure()); document.getElementById('schedule')?.addEventListener('click', () => this.testSchedule()); - document.getElementById('alarm-status')?.addEventListener('click', () => this.testAlarmStatus()); - document.getElementById('request-permission')?.addEventListener('click', () => this.testRequestPermission()); + document.getElementById('endorser-api')?.addEventListener('click', () => this.testEndorserAPI()); + document.getElementById('callbacks')?.addEventListener('click', () => this.testCallbacks()); + document.getElementById('status')?.addEventListener('click', () => this.testStatus()); document.getElementById('performance')?.addEventListener('click', () => this.testPerformance()); document.getElementById('clear-log')?.addEventListener('click', () => this.clearLog()); } private async testConfigure() { try { - this.log('Testing configuration...'); - await DailyNotification.configure({ - storage: 'shared', - ttlSeconds: 1800, - prefetchLeadMinutes: 15, - enableETagSupport: true, - enableErrorHandling: true, - enablePerformanceOptimization: true + this.log('Testing TimeSafari configuration...'); + await this.configLoader.loadConfig(); + const config = this.configLoader.getConfig(); + + await this.notificationService.initialize(); + + this.log('✅ TimeSafari configuration successful', { + appId: config.timesafari.appId, + appName: config.timesafari.appName, + version: config.timesafari.version }); - this.log('✅ Configuration successful'); this.updateStatus('Configured'); } catch (error) { this.log(`❌ Configuration failed: ${error}`); @@ -80,53 +50,271 @@ class TestApp { private async testSchedule() { try { - this.log('Testing notification scheduling...'); - await DailyNotification.scheduleDailyNotification({ - url: 'https://api.example.com/daily-content', - time: '09:00', - title: 'Daily Test Notification', - body: 'This is a test notification from the Android test app' - }); - this.log('✅ Notification scheduled successfully'); + this.log('Testing TimeSafari community notification scheduling...'); + const config = this.configLoader.getConfig(); + + const dualConfig = { + contentFetch: { + enabled: true, + schedule: config.scheduling.contentFetch.schedule, + url: this.configLoader.getEndorserUrl('notificationsBundle'), + headers: this.configLoader.getAuthHeaders(), + ttlSeconds: 3600, + timeout: 30000, + retryAttempts: 3, + retryDelay: 5000, + callbacks: { + onSuccess: async (data: any) => { + this.log('✅ Content fetch successful', data); + await this.processEndorserNotificationBundle(data); + }, + onError: async (error: any) => { + this.log('❌ Content fetch failed', error); + } + } + }, + userNotification: { + enabled: true, + schedule: config.scheduling.userNotification.schedule, + title: 'TimeSafari Community Update', + body: 'New offers, projects, people, and items await your attention!', + 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, + fallbackBehavior: 'show_default' + } + }; + + await this.notificationService.scheduleDualNotification(dualConfig); + this.log('✅ Community notification scheduled successfully'); this.updateStatus('Scheduled'); } catch (error) { this.log(`❌ Scheduling failed: ${error}`); } } - private async testAlarmStatus() { + private async testEndorserAPI() { try { - this.log('Testing exact alarm status...'); - const status = await DailyNotification.getExactAlarmStatus(); - this.log(`📱 Alarm Status:`, status); - this.updateStatus(`Alarm: ${status.canSchedule ? 'Enabled' : 'Disabled'}`); + this.log('Testing Endorser.ch API integration...'); + const config = this.configLoader.getConfig(); + const testData = config.testData; + + // Test parallel API requests pattern + const requests = [ + // Offers to person + fetch(`${this.configLoader.getEndorserUrl('offers')}?recipientId=${testData.userDid}&afterId=${testData.lastKnownOfferId}`, { + headers: this.configLoader.getAuthHeaders() + }), + + // Offers to user's projects + fetch(`${this.configLoader.getEndorserUrl('offersToPlans')}?afterId=${testData.lastKnownOfferId}`, { + headers: this.configLoader.getAuthHeaders() + }), + + // Changes to starred projects + fetch(this.configLoader.getEndorserUrl('plansLastUpdated'), { + method: 'POST', + headers: this.configLoader.getAuthHeaders(), + body: JSON.stringify({ + planIds: testData.starredPlanIds, + afterId: testData.lastKnownPlanId + }) + }) + ]; + + const [offersToPerson, offersToProjects, starredChanges] = await Promise.all(requests); + + const notificationData = { + offersToPerson: await offersToPerson.json(), + offersToProjects: await offersToProjects.json(), + starredChanges: await starredChanges.json() + }; + + this.log('✅ Endorser.ch API integration successful', { + offersToPerson: notificationData.offersToPerson.data?.length || 0, + offersToProjects: notificationData.offersToProjects.data?.length || 0, + starredChanges: notificationData.starredChanges.data?.length || 0 + }); + + this.updateStatus('API Connected'); } catch (error) { - this.log(`❌ Alarm status check failed: ${error}`); + this.log(`❌ Endorser.ch API test failed: ${error}`); } } - private async testRequestPermission() { + private async testCallbacks() { try { - this.log('Testing permission request...'); - await DailyNotification.requestExactAlarmPermission(); - this.log('✅ Permission request sent'); - this.updateStatus('Permission Requested'); + this.log('Testing TimeSafari notification callbacks...'); + const config = this.configLoader.getConfig(); + + // Register offers callback + await this.notificationService.registerCallback('offers', async (event: any) => { + this.log('📨 Offers callback triggered', event); + await this.handleOffersNotification(event); + }); + + // Register projects callback + await this.notificationService.registerCallback('projects', async (event: any) => { + this.log('📨 Projects callback triggered', event); + await this.handleProjectsNotification(event); + }); + + // Register people callback + await this.notificationService.registerCallback('people', async (event: any) => { + this.log('📨 People callback triggered', event); + await this.handlePeopleNotification(event); + }); + + // Register items callback + await this.notificationService.registerCallback('items', async (event: any) => { + this.log('📨 Items callback triggered', event); + await this.handleItemsNotification(event); + }); + + this.log('✅ All callbacks registered successfully'); + this.updateStatus('Callbacks Registered'); + } catch (error) { + this.log(`❌ Callback registration failed: ${error}`); + } + } + + private async testStatus() { + try { + this.log('Testing notification status...'); + const status = await this.notificationService.getDualScheduleStatus(); + this.log('📊 Notification Status:', status); + this.updateStatus(`Status: ${status.contentFetch.enabled ? 'Active' : 'Inactive'}`); } catch (error) { - this.log(`❌ Permission request failed: ${error}`); + this.log(`❌ Status check failed: ${error}`); } } private async testPerformance() { try { - this.log('Testing performance metrics...'); - const metrics = await DailyNotification.getPerformanceMetrics(); - this.log(`📊 Performance Metrics:`, metrics); + this.log('Testing Android performance metrics...'); + const metrics = { + overallScore: 85, + databasePerformance: 90, + memoryEfficiency: 80, + batteryEfficiency: 85, + objectPoolEfficiency: 90, + totalDatabaseQueries: 150, + averageMemoryUsage: 25.5, + objectPoolHits: 45, + backgroundCpuUsage: 2.3, + totalNetworkRequests: 12, + recommendations: ['Enable ETag support', 'Optimize memory usage'] + }; + + this.log('📊 Android Performance Metrics:', metrics); this.updateStatus(`Performance: ${metrics.overallScore}/100`); } catch (error) { this.log(`❌ Performance check failed: ${error}`); } } + /** + * Process Endorser.ch notification bundle using parallel API requests + */ + private async processEndorserNotificationBundle(data: any): Promise { + try { + this.log('Processing Endorser.ch notification bundle...'); + + // Process each notification type + if (data.offersToPerson?.data?.length > 0) { + await this.handleOffersNotification(data.offersToPerson); + } + + if (data.starredChanges?.data?.length > 0) { + await this.handleProjectsNotification(data.starredChanges); + } + + this.log('✅ Notification bundle processed successfully'); + } catch (error) { + this.log(`❌ Bundle processing failed: ${error}`); + } + } + + /** + * Handle offers notification events from Endorser.ch API + */ + private async handleOffersNotification(event: any): Promise { + this.log('Handling offers notification:', event); + + if (event.data && event.data.length > 0) { + // Process OfferSummaryArrayMaybeMoreBody format + event.data.forEach((offer: any) => { + this.log('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]; + this.log('More offers available, last JWT ID:', lastOffer.jwtId); + } + } + } + + /** + * Handle projects notification events from Endorser.ch API + */ + private async handleProjectsNotification(event: any): Promise { + this.log('Handling projects notification:', event); + + if (event.data && event.data.length > 0) { + // Process PlanSummaryAndPreviousClaimArrayMaybeMore format + event.data.forEach((planData: any) => { + const { plan, wrappedClaimBefore } = planData; + this.log('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]; + this.log('More project changes available, last JWT ID:', lastPlan.plan.jwtId); + } + } + } + + /** + * Handle people notification events + */ + private async handlePeopleNotification(event: any): Promise { + this.log('Handling people notification:', event); + // Implementation would process people data and update local state + } + + /** + * Handle items notification events + */ + private async handleItemsNotification(event: any): Promise { + this.log('Handling items notification:', event); + // Implementation would process items data and update local state + } + private log(message: string, data?: any) { const timestamp = new Date().toLocaleTimeString(); const logEntry = document.createElement('div'); @@ -150,5 +338,5 @@ class TestApp { // Initialize app when DOM is ready document.addEventListener('DOMContentLoaded', () => { - new TestApp(); + new TimeSafariAndroidTestApp(); }); diff --git a/test-apps/config/timesafari-config.json b/test-apps/config/timesafari-config.json new file mode 100644 index 0000000..9303e1a --- /dev/null +++ b/test-apps/config/timesafari-config.json @@ -0,0 +1,152 @@ +{ + "timesafari": { + "appId": "app.timesafari.test", + "appName": "TimeSafari Test", + "version": "1.0.0", + "description": "Test app for TimeSafari Daily Notification Plugin integration" + }, + "endorser": { + "baseUrl": "http://localhost:3001", + "apiVersion": "v2", + "endpoints": { + "offers": "/api/v2/report/offers", + "offersToPlans": "/api/v2/report/offersToPlansOwnedByMe", + "plansLastUpdated": "/api/v2/report/plansLastUpdatedBetween", + "notificationsBundle": "/api/v2/report/notifications/bundle" + }, + "authentication": { + "type": "Bearer", + "token": "test-jwt-token-12345", + "headers": { + "Authorization": "Bearer test-jwt-token-12345", + "Content-Type": "application/json", + "X-Privacy-Level": "user-controlled" + } + }, + "pagination": { + "defaultLimit": 50, + "maxLimit": 100, + "hitLimitThreshold": 50 + } + }, + "notificationTypes": { + "offers": { + "enabled": true, + "types": [ + "new_to_me", + "changed_to_me", + "new_to_projects", + "changed_to_projects", + "new_to_favorites", + "changed_to_favorites" + ] + }, + "projects": { + "enabled": true, + "types": [ + "local_new", + "local_changed", + "content_interest_new", + "favorited_changed" + ] + }, + "people": { + "enabled": true, + "types": [ + "local_new", + "local_changed", + "content_interest_new", + "favorited_changed", + "contacts_changed" + ] + }, + "items": { + "enabled": true, + "types": [ + "local_new", + "local_changed", + "favorited_changed" + ] + } + }, + "scheduling": { + "contentFetch": { + "schedule": "0 8 * * *", + "time": "08:00", + "description": "8 AM daily - fetch community updates" + }, + "userNotification": { + "schedule": "0 9 * * *", + "time": "09:00", + "description": "9 AM daily - notify users of community updates" + } + }, + "testData": { + "userDid": "did:example:testuser123", + "starredPlanIds": [ + "plan-community-garden", + "plan-local-food", + "plan-sustainability" + ], + "lastKnownOfferId": "01HSE3R9MAC0FT3P3KZ382TWV7", + "lastKnownPlanId": "01HSE3R9MAC0FT3P3KZ382TWV8", + "mockOffers": [ + { + "jwtId": "01HSE3R9MAC0FT3P3KZ382TWV7", + "handleId": "offer-web-dev-001", + "offeredByDid": "did:example:offerer123", + "recipientDid": "did:example:testuser123", + "objectDescription": "Web development services for community project", + "unit": "USD", + "amount": 1000, + "amountGiven": 500, + "amountGivenConfirmed": 250 + } + ], + "mockProjects": [ + { + "plan": { + "jwtId": "01HSE3R9MAC0FT3P3KZ382TWV8", + "handleId": "plan-community-garden", + "name": "Community Garden Project", + "description": "Building a community garden for local food production", + "issuerDid": "did:example:issuer123", + "agentDid": "did:example:agent123" + }, + "wrappedClaimBefore": null + } + ] + }, + "callbacks": { + "offers": { + "enabled": true, + "localHandler": "handleOffersNotification" + }, + "projects": { + "enabled": true, + "localHandler": "handleProjectsNotification" + }, + "people": { + "enabled": true, + "localHandler": "handlePeopleNotification" + }, + "items": { + "enabled": true, + "localHandler": "handleItemsNotification" + }, + "communityAnalytics": { + "enabled": true, + "endpoint": "http://localhost:3001/api/analytics/community-events", + "headers": { + "Content-Type": "application/json", + "X-Privacy-Level": "aggregated" + } + } + }, + "observability": { + "enableLogging": true, + "logLevel": "debug", + "enableMetrics": true, + "enableHealthChecks": true + } +} diff --git a/test-apps/electron-test/src/index.html b/test-apps/electron-test/src/index.html index 2a3fea4..993a99e 100644 --- a/test-apps/electron-test/src/index.html +++ b/test-apps/electron-test/src/index.html @@ -89,13 +89,15 @@
-

⚡ Daily Notification Plugin - Electron Test

+

⚡ TimeSafari Daily Notification - Electron Test

Ready
- - + + + +
diff --git a/test-apps/electron-test/src/index.ts b/test-apps/electron-test/src/index.ts index 3aa66e7..5df716d 100644 --- a/test-apps/electron-test/src/index.ts +++ b/test-apps/electron-test/src/index.ts @@ -1,18 +1,28 @@ -// Electron test interface -class TestApp { +import { ConfigLoader, MockDailyNotificationService, TestLogger } from '../shared/config-loader'; + +// Test interface for TimeSafari Electron integration +class TimeSafariElectronTestApp { private statusElement: HTMLElement; private logElement: HTMLElement; + private configLoader: ConfigLoader; + private notificationService: MockDailyNotificationService; + private logger: TestLogger; constructor() { this.statusElement = document.getElementById('status')!; this.logElement = document.getElementById('log')!; + this.configLoader = ConfigLoader.getInstance(); + this.logger = new TestLogger('debug'); + this.notificationService = new MockDailyNotificationService(this.configLoader.getConfig()); this.setupEventListeners(); - this.log('Electron Test app initialized'); + this.log('TimeSafari Electron Test app initialized'); } private setupEventListeners() { document.getElementById('configure')?.addEventListener('click', () => this.testConfigure()); document.getElementById('schedule')?.addEventListener('click', () => this.testSchedule()); + document.getElementById('endorser-api')?.addEventListener('click', () => this.testEndorserAPI()); + document.getElementById('callbacks')?.addEventListener('click', () => this.testCallbacks()); document.getElementById('debug-info')?.addEventListener('click', () => this.testDebugInfo()); document.getElementById('performance')?.addEventListener('click', () => this.testPerformance()); document.getElementById('clear-log')?.addEventListener('click', () => this.clearLog()); @@ -20,80 +30,299 @@ class TestApp { private async testConfigure() { try { - this.log('Testing Electron configuration...'); - const result = await (window as any).electronAPI.configurePlugin({ - storage: 'mock', - ttlSeconds: 1800, - prefetchLeadMinutes: 15, - enableETagSupport: true, - enableErrorHandling: true, - enablePerformanceOptimization: true - }); + this.log('Testing TimeSafari Electron configuration...'); + await this.configLoader.loadConfig(); + const config = this.configLoader.getConfig(); - if (result.success) { - this.log('✅ Electron Configuration successful'); - this.updateStatus('Configured'); - } else { - this.log(`❌ Configuration failed: ${result.error}`); - } + await this.notificationService.initialize(); + + this.log('✅ TimeSafari Electron configuration successful', { + appId: config.timesafari.appId, + appName: config.timesafari.appName, + version: config.timesafari.version + }); + this.updateStatus('Configured'); } catch (error) { - this.log(`❌ Configuration error: ${error}`); + this.log(`❌ Configuration failed: ${error}`); } } private async testSchedule() { try { - this.log('Testing Electron notification scheduling...'); - const result = await (window as any).electronAPI.scheduleNotification({ - url: 'https://api.example.com/daily-content', - time: '09:00', - title: 'Daily Electron Test Notification', - body: 'This is a test notification from the Electron test app' + this.log('Testing TimeSafari Electron community notification scheduling...'); + const config = this.configLoader.getConfig(); + + const dualConfig = { + contentFetch: { + enabled: true, + schedule: config.scheduling.contentFetch.schedule, + url: this.configLoader.getEndorserUrl('notificationsBundle'), + headers: this.configLoader.getAuthHeaders(), + ttlSeconds: 3600, + timeout: 30000, + retryAttempts: 3, + retryDelay: 5000, + callbacks: { + onSuccess: async (data: any) => { + this.log('✅ Content fetch successful', data); + await this.processEndorserNotificationBundle(data); + }, + onError: async (error: any) => { + this.log('❌ Content fetch failed', error); + } + } + }, + userNotification: { + enabled: true, + schedule: config.scheduling.userNotification.schedule, + title: 'TimeSafari Community Update', + body: 'New offers, projects, people, and items await your attention!', + 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, + fallbackBehavior: 'show_default' + } + }; + + await this.notificationService.scheduleDualNotification(dualConfig); + this.log('✅ Electron community notification scheduled successfully'); + this.updateStatus('Scheduled'); + } catch (error) { + this.log(`❌ Electron scheduling failed: ${error}`); + } + } + + private async testEndorserAPI() { + try { + this.log('Testing Endorser.ch API integration on Electron...'); + const config = this.configLoader.getConfig(); + const testData = config.testData; + + // Test parallel API requests pattern + const requests = [ + // Offers to person + fetch(`${this.configLoader.getEndorserUrl('offers')}?recipientId=${testData.userDid}&afterId=${testData.lastKnownOfferId}`, { + headers: this.configLoader.getAuthHeaders() + }), + + // Offers to user's projects + fetch(`${this.configLoader.getEndorserUrl('offersToPlans')}?afterId=${testData.lastKnownOfferId}`, { + headers: this.configLoader.getAuthHeaders() + }), + + // Changes to starred projects + fetch(this.configLoader.getEndorserUrl('plansLastUpdated'), { + method: 'POST', + headers: this.configLoader.getAuthHeaders(), + body: JSON.stringify({ + planIds: testData.starredPlanIds, + afterId: testData.lastKnownPlanId + }) + }) + ]; + + const [offersToPerson, offersToProjects, starredChanges] = await Promise.all(requests); + + const notificationData = { + offersToPerson: await offersToPerson.json(), + offersToProjects: await offersToProjects.json(), + starredChanges: await starredChanges.json() + }; + + this.log('✅ Endorser.ch API integration successful on Electron', { + offersToPerson: notificationData.offersToPerson.data?.length || 0, + offersToProjects: notificationData.offersToProjects.data?.length || 0, + starredChanges: notificationData.starredChanges.data?.length || 0 }); - if (result.success) { - this.log('✅ Electron Notification scheduled successfully'); - this.updateStatus('Scheduled'); - } else { - this.log(`❌ Scheduling failed: ${result.error}`); - } + this.updateStatus('API Connected'); } catch (error) { - this.log(`❌ Scheduling error: ${error}`); + this.log(`❌ Endorser.ch API test failed: ${error}`); + } + } + + private async testCallbacks() { + try { + this.log('Testing TimeSafari Electron notification callbacks...'); + const config = this.configLoader.getConfig(); + + // Register offers callback + await this.notificationService.registerCallback('offers', async (event: any) => { + this.log('📨 Electron Offers callback triggered', event); + await this.handleOffersNotification(event); + }); + + // Register projects callback + await this.notificationService.registerCallback('projects', async (event: any) => { + this.log('📨 Electron Projects callback triggered', event); + await this.handleProjectsNotification(event); + }); + + // Register people callback + await this.notificationService.registerCallback('people', async (event: any) => { + this.log('📨 Electron People callback triggered', event); + await this.handlePeopleNotification(event); + }); + + // Register items callback + await this.notificationService.registerCallback('items', async (event: any) => { + this.log('📨 Electron Items callback triggered', event); + await this.handleItemsNotification(event); + }); + + this.log('✅ All Electron callbacks registered successfully'); + this.updateStatus('Callbacks Registered'); + } catch (error) { + this.log(`❌ Electron callback registration failed: ${error}`); } } private async testDebugInfo() { try { this.log('Testing Electron debug info...'); - const result = await (window as any).electronAPI.getDebugInfo(); + const debugInfo = { + platform: 'electron', + nodeVersion: process.versions.node, + electronVersion: process.versions.electron, + chromeVersion: process.versions.chrome, + status: 'running', + config: this.configLoader.getConfig().timesafari, + timestamp: new Date().toISOString() + }; - if (result.success) { - this.log('🔍 Electron Debug Info:', result.data); - this.updateStatus(`Debug: ${result.data.status}`); - } else { - this.log(`❌ Debug info failed: ${result.error}`); - } + this.log('🔍 Electron Debug Info:', debugInfo); + this.updateStatus(`Debug: ${debugInfo.status}`); } catch (error) { - this.log(`❌ Debug info error: ${error}`); + this.log(`❌ Debug info failed: ${error}`); } } private async testPerformance() { try { this.log('Testing Electron performance metrics...'); - const result = await (window as any).electronAPI.getPerformanceMetrics(); + const metrics = { + overallScore: 82, + databasePerformance: 85, + memoryEfficiency: 78, + batteryEfficiency: 80, + objectPoolEfficiency: 85, + totalDatabaseQueries: 100, + averageMemoryUsage: 30.2, + objectPoolHits: 25, + backgroundCpuUsage: 3.1, + totalNetworkRequests: 15, + recommendations: ['Optimize IPC communication', 'Reduce memory usage'] + }; + + this.log('📊 Electron Performance Metrics:', metrics); + this.updateStatus(`Performance: ${metrics.overallScore}/100`); + } catch (error) { + this.log(`❌ Performance check failed: ${error}`); + } + } + + /** + * Process Endorser.ch notification bundle using parallel API requests + */ + private async processEndorserNotificationBundle(data: any): Promise { + try { + this.log('Processing Endorser.ch notification bundle on Electron...'); + + // Process each notification type + if (data.offersToPerson?.data?.length > 0) { + await this.handleOffersNotification(data.offersToPerson); + } - if (result.success) { - this.log('📊 Electron Performance Metrics:', result.data); - this.updateStatus(`Performance: ${result.data.overallScore}/100`); - } else { - this.log(`❌ Performance check failed: ${result.error}`); + if (data.starredChanges?.data?.length > 0) { + await this.handleProjectsNotification(data.starredChanges); } + + this.log('✅ Electron notification bundle processed successfully'); } catch (error) { - this.log(`❌ Performance error: ${error}`); + this.log(`❌ Electron bundle processing failed: ${error}`); + } + } + + /** + * Handle offers notification events from Endorser.ch API + */ + private async handleOffersNotification(event: any): Promise { + this.log('Handling Electron offers notification:', event); + + if (event.data && event.data.length > 0) { + // Process OfferSummaryArrayMaybeMoreBody format + event.data.forEach((offer: any) => { + this.log('Processing Electron 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]; + this.log('More offers available, last JWT ID:', lastOffer.jwtId); + } + } + } + + /** + * Handle projects notification events from Endorser.ch API + */ + private async handleProjectsNotification(event: any): Promise { + this.log('Handling Electron projects notification:', event); + + if (event.data && event.data.length > 0) { + // Process PlanSummaryAndPreviousClaimArrayMaybeMore format + event.data.forEach((planData: any) => { + const { plan, wrappedClaimBefore } = planData; + this.log('Processing Electron 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]; + this.log('More project changes available, last JWT ID:', lastPlan.plan.jwtId); + } } } + /** + * Handle people notification events + */ + private async handlePeopleNotification(event: any): Promise { + this.log('Handling Electron people notification:', event); + // Implementation would process people data and update local state + } + + /** + * Handle items notification events + */ + private async handleItemsNotification(event: any): Promise { + this.log('Handling Electron items notification:', event); + // Implementation would process items data and update local state + } + private log(message: string, data?: any) { const timestamp = new Date().toLocaleTimeString(); const logEntry = document.createElement('div'); @@ -117,5 +346,5 @@ class TestApp { // Initialize app when DOM is ready document.addEventListener('DOMContentLoaded', () => { - new TestApp(); + new TimeSafariElectronTestApp(); }); diff --git a/test-apps/ios-test/src/index.html b/test-apps/ios-test/src/index.html index 892e770..ff1e813 100644 --- a/test-apps/ios-test/src/index.html +++ b/test-apps/ios-test/src/index.html @@ -89,15 +89,16 @@
-

🍎 Daily Notification Plugin - iOS Test

+

🍎 TimeSafari Daily Notification - iOS Test

Ready
- - - - + + + + +
diff --git a/test-apps/ios-test/src/index.ts b/test-apps/ios-test/src/index.ts index c192c43..20cb10f 100644 --- a/test-apps/ios-test/src/index.ts +++ b/test-apps/ios-test/src/index.ts @@ -1,76 +1,47 @@ import { Capacitor } from '@capacitor/core'; +import { ConfigLoader, MockDailyNotificationService, TestLogger } from '../shared/config-loader'; -// Mock plugin for development -const DailyNotification = { - async configure(options: any) { - console.log('Configure called:', options); - return Promise.resolve(); - }, - async scheduleDailyNotification(options: any) { - console.log('Schedule called:', options); - return Promise.resolve(); - }, - async maintainRollingWindow() { - console.log('Maintain rolling window called'); - return Promise.resolve(); - }, - async getRollingWindowStats() { - return Promise.resolve({ - stats: '64 pending notifications, 20 daily limit', - maintenanceNeeded: false, - timeUntilNextMaintenance: 900000 - }); - }, - async getPerformanceMetrics() { - return Promise.resolve({ - overallScore: 88, - databasePerformance: 92, - memoryEfficiency: 85, - batteryEfficiency: 90, - objectPoolEfficiency: 88, - totalDatabaseQueries: 120, - averageMemoryUsage: 22.3, - objectPoolHits: 38, - backgroundCpuUsage: 1.8, - totalNetworkRequests: 8, - recommendations: ['Enable background tasks', 'Optimize memory usage'] - }); - } -}; - -// Test interface -class TestApp { +// Test interface for TimeSafari iOS integration +class TimeSafariIOSTestApp { private statusElement: HTMLElement; private logElement: HTMLElement; + private configLoader: ConfigLoader; + private notificationService: MockDailyNotificationService; + private logger: TestLogger; constructor() { this.statusElement = document.getElementById('status')!; this.logElement = document.getElementById('log')!; + this.configLoader = ConfigLoader.getInstance(); + this.logger = new TestLogger('debug'); + this.notificationService = new MockDailyNotificationService(this.configLoader.getConfig()); this.setupEventListeners(); - this.log('iOS Test app initialized'); + this.log('TimeSafari iOS Test app initialized'); } private setupEventListeners() { document.getElementById('configure')?.addEventListener('click', () => this.testConfigure()); document.getElementById('schedule')?.addEventListener('click', () => this.testSchedule()); document.getElementById('rolling-window')?.addEventListener('click', () => this.testRollingWindow()); - document.getElementById('window-stats')?.addEventListener('click', () => this.testWindowStats()); + document.getElementById('endorser-api')?.addEventListener('click', () => this.testEndorserAPI()); + document.getElementById('callbacks')?.addEventListener('click', () => this.testCallbacks()); document.getElementById('performance')?.addEventListener('click', () => this.testPerformance()); document.getElementById('clear-log')?.addEventListener('click', () => this.clearLog()); } private async testConfigure() { try { - this.log('Testing iOS configuration...'); - await DailyNotification.configure({ - storage: 'shared', - ttlSeconds: 1800, - prefetchLeadMinutes: 15, - enableETagSupport: true, - enableErrorHandling: true, - enablePerformanceOptimization: true + this.log('Testing TimeSafari iOS configuration...'); + await this.configLoader.loadConfig(); + const config = this.configLoader.getConfig(); + + await this.notificationService.initialize(); + + this.log('✅ TimeSafari iOS configuration successful', { + appId: config.timesafari.appId, + appName: config.timesafari.appName, + version: config.timesafari.version }); - this.log('✅ iOS Configuration successful'); this.updateStatus('Configured'); } catch (error) { this.log(`❌ Configuration failed: ${error}`); @@ -79,53 +50,277 @@ class TestApp { private async testSchedule() { try { - this.log('Testing iOS notification scheduling...'); - await DailyNotification.scheduleDailyNotification({ - url: 'https://api.example.com/daily-content', - time: '09:00', - title: 'Daily iOS Test Notification', - body: 'This is a test notification from the iOS test app' - }); - this.log('✅ iOS Notification scheduled successfully'); + this.log('Testing TimeSafari iOS community notification scheduling...'); + const config = this.configLoader.getConfig(); + + const dualConfig = { + contentFetch: { + enabled: true, + schedule: config.scheduling.contentFetch.schedule, + url: this.configLoader.getEndorserUrl('notificationsBundle'), + headers: this.configLoader.getAuthHeaders(), + ttlSeconds: 3600, + timeout: 30000, + retryAttempts: 3, + retryDelay: 5000, + callbacks: { + onSuccess: async (data: any) => { + this.log('✅ Content fetch successful', data); + await this.processEndorserNotificationBundle(data); + }, + onError: async (error: any) => { + this.log('❌ Content fetch failed', error); + } + } + }, + userNotification: { + enabled: true, + schedule: config.scheduling.userNotification.schedule, + title: 'TimeSafari Community Update', + body: 'New offers, projects, people, and items await your attention!', + 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, + fallbackBehavior: 'show_default' + } + }; + + await this.notificationService.scheduleDualNotification(dualConfig); + this.log('✅ iOS community notification scheduled successfully'); this.updateStatus('Scheduled'); } catch (error) { - this.log(`❌ iOS Scheduling failed: ${error}`); + this.log(`❌ iOS scheduling failed: ${error}`); } } private async testRollingWindow() { try { this.log('Testing iOS rolling window maintenance...'); - await DailyNotification.maintainRollingWindow(); - this.log('✅ Rolling window maintenance completed'); + // Simulate rolling window maintenance + const stats = { + stats: '64 pending notifications, 20 daily limit', + maintenanceNeeded: false, + timeUntilNextMaintenance: 900000 + }; + + this.log('✅ Rolling window maintenance completed', stats); this.updateStatus('Rolling Window Maintained'); } catch (error) { this.log(`❌ Rolling window maintenance failed: ${error}`); } } - private async testWindowStats() { + private async testEndorserAPI() { try { - this.log('Testing iOS rolling window stats...'); - const stats = await DailyNotification.getRollingWindowStats(); - this.log(`📊 Rolling Window Stats:`, stats); - this.updateStatus(`Window: ${stats.maintenanceNeeded ? 'Needs Maintenance' : 'OK'}`); + this.log('Testing Endorser.ch API integration on iOS...'); + const config = this.configLoader.getConfig(); + const testData = config.testData; + + // Test parallel API requests pattern + const requests = [ + // Offers to person + fetch(`${this.configLoader.getEndorserUrl('offers')}?recipientId=${testData.userDid}&afterId=${testData.lastKnownOfferId}`, { + headers: this.configLoader.getAuthHeaders() + }), + + // Offers to user's projects + fetch(`${this.configLoader.getEndorserUrl('offersToPlans')}?afterId=${testData.lastKnownOfferId}`, { + headers: this.configLoader.getAuthHeaders() + }), + + // Changes to starred projects + fetch(this.configLoader.getEndorserUrl('plansLastUpdated'), { + method: 'POST', + headers: this.configLoader.getAuthHeaders(), + body: JSON.stringify({ + planIds: testData.starredPlanIds, + afterId: testData.lastKnownPlanId + }) + }) + ]; + + const [offersToPerson, offersToProjects, starredChanges] = await Promise.all(requests); + + const notificationData = { + offersToPerson: await offersToPerson.json(), + offersToProjects: await offersToProjects.json(), + starredChanges: await starredChanges.json() + }; + + this.log('✅ Endorser.ch API integration successful on iOS', { + offersToPerson: notificationData.offersToPerson.data?.length || 0, + offersToProjects: notificationData.offersToProjects.data?.length || 0, + starredChanges: notificationData.starredChanges.data?.length || 0 + }); + + this.updateStatus('API Connected'); } catch (error) { - this.log(`❌ Window stats check failed: ${error}`); + this.log(`❌ Endorser.ch API test failed: ${error}`); + } + } + + private async testCallbacks() { + try { + this.log('Testing TimeSafari iOS notification callbacks...'); + const config = this.configLoader.getConfig(); + + // Register offers callback + await this.notificationService.registerCallback('offers', async (event: any) => { + this.log('📨 iOS Offers callback triggered', event); + await this.handleOffersNotification(event); + }); + + // Register projects callback + await this.notificationService.registerCallback('projects', async (event: any) => { + this.log('📨 iOS Projects callback triggered', event); + await this.handleProjectsNotification(event); + }); + + // Register people callback + await this.notificationService.registerCallback('people', async (event: any) => { + this.log('📨 iOS People callback triggered', event); + await this.handlePeopleNotification(event); + }); + + // Register items callback + await this.notificationService.registerCallback('items', async (event: any) => { + this.log('📨 iOS Items callback triggered', event); + await this.handleItemsNotification(event); + }); + + this.log('✅ All iOS callbacks registered successfully'); + this.updateStatus('Callbacks Registered'); + } catch (error) { + this.log(`❌ iOS callback registration failed: ${error}`); } } private async testPerformance() { try { this.log('Testing iOS performance metrics...'); - const metrics = await DailyNotification.getPerformanceMetrics(); - this.log(`📊 iOS Performance Metrics:`, metrics); + const metrics = { + overallScore: 88, + databasePerformance: 92, + memoryEfficiency: 85, + batteryEfficiency: 90, + objectPoolEfficiency: 88, + totalDatabaseQueries: 120, + averageMemoryUsage: 22.3, + objectPoolHits: 38, + backgroundCpuUsage: 1.8, + totalNetworkRequests: 8, + recommendations: ['Enable background tasks', 'Optimize memory usage'] + }; + + this.log('📊 iOS Performance Metrics:', metrics); this.updateStatus(`Performance: ${metrics.overallScore}/100`); } catch (error) { this.log(`❌ Performance check failed: ${error}`); } } + /** + * Process Endorser.ch notification bundle using parallel API requests + */ + private async processEndorserNotificationBundle(data: any): Promise { + try { + this.log('Processing Endorser.ch notification bundle on iOS...'); + + // Process each notification type + if (data.offersToPerson?.data?.length > 0) { + await this.handleOffersNotification(data.offersToPerson); + } + + if (data.starredChanges?.data?.length > 0) { + await this.handleProjectsNotification(data.starredChanges); + } + + this.log('✅ iOS notification bundle processed successfully'); + } catch (error) { + this.log(`❌ iOS bundle processing failed: ${error}`); + } + } + + /** + * Handle offers notification events from Endorser.ch API + */ + private async handleOffersNotification(event: any): Promise { + this.log('Handling iOS offers notification:', event); + + if (event.data && event.data.length > 0) { + // Process OfferSummaryArrayMaybeMoreBody format + event.data.forEach((offer: any) => { + this.log('Processing iOS 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]; + this.log('More offers available, last JWT ID:', lastOffer.jwtId); + } + } + } + + /** + * Handle projects notification events from Endorser.ch API + */ + private async handleProjectsNotification(event: any): Promise { + this.log('Handling iOS projects notification:', event); + + if (event.data && event.data.length > 0) { + // Process PlanSummaryAndPreviousClaimArrayMaybeMore format + event.data.forEach((planData: any) => { + const { plan, wrappedClaimBefore } = planData; + this.log('Processing iOS 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]; + this.log('More project changes available, last JWT ID:', lastPlan.plan.jwtId); + } + } + } + + /** + * Handle people notification events + */ + private async handlePeopleNotification(event: any): Promise { + this.log('Handling iOS people notification:', event); + // Implementation would process people data and update local state + } + + /** + * Handle items notification events + */ + private async handleItemsNotification(event: any): Promise { + this.log('Handling iOS items notification:', event); + // Implementation would process items data and update local state + } + private log(message: string, data?: any) { const timestamp = new Date().toLocaleTimeString(); const logEntry = document.createElement('div'); @@ -149,5 +344,5 @@ class TestApp { // Initialize app when DOM is ready document.addEventListener('DOMContentLoaded', () => { - new TestApp(); + new TimeSafariIOSTestApp(); }); diff --git a/test-apps/shared/config-loader.ts b/test-apps/shared/config-loader.ts new file mode 100644 index 0000000..8e9dae6 --- /dev/null +++ b/test-apps/shared/config-loader.ts @@ -0,0 +1,522 @@ +/** + * Configuration loader for TimeSafari test apps + * + * Loads configuration from JSON files and provides typed access + * to TimeSafari-specific settings, Endorser.ch API endpoints, + * and test data. + * + * @author Matthew Raymer + * @version 1.0.0 + */ + +export interface TimeSafariConfig { + timesafari: { + appId: string; + appName: string; + version: string; + description: string; + }; + endorser: { + baseUrl: string; + apiVersion: string; + endpoints: { + offers: string; + offersToPlans: string; + plansLastUpdated: string; + notificationsBundle: string; + }; + authentication: { + type: string; + token: string; + headers: Record; + }; + pagination: { + defaultLimit: number; + maxLimit: number; + hitLimitThreshold: number; + }; + }; + notificationTypes: { + offers: { + enabled: boolean; + types: string[]; + }; + projects: { + enabled: boolean; + types: string[]; + }; + people: { + enabled: boolean; + types: string[]; + }; + items: { + enabled: boolean; + types: string[]; + }; + }; + scheduling: { + contentFetch: { + schedule: string; + time: string; + description: string; + }; + userNotification: { + schedule: string; + time: string; + description: string; + }; + }; + testData: { + userDid: string; + starredPlanIds: string[]; + lastKnownOfferId: string; + lastKnownPlanId: string; + mockOffers: any[]; + mockProjects: any[]; + }; + callbacks: { + offers: { + enabled: boolean; + localHandler: string; + }; + projects: { + enabled: boolean; + localHandler: string; + }; + people: { + enabled: boolean; + localHandler: string; + }; + items: { + enabled: boolean; + localHandler: string; + }; + communityAnalytics: { + enabled: boolean; + endpoint: string; + headers: Record; + }; + }; + observability: { + enableLogging: boolean; + logLevel: string; + enableMetrics: boolean; + enableHealthChecks: boolean; + }; +} + +/** + * Configuration loader class + */ +export class ConfigLoader { + private static instance: ConfigLoader; + private config: TimeSafariConfig | null = null; + + private constructor() {} + + /** + * Get singleton instance + */ + public static getInstance(): ConfigLoader { + if (!ConfigLoader.instance) { + ConfigLoader.instance = new ConfigLoader(); + } + return ConfigLoader.instance; + } + + /** + * Load configuration from JSON file + */ + public async loadConfig(): Promise { + if (this.config) { + return this.config; + } + + try { + // In a real app, this would fetch from a config file + // For test apps, we'll use a hardcoded config + this.config = { + timesafari: { + appId: "app.timesafari.test", + appName: "TimeSafari Test", + version: "1.0.0", + description: "Test app for TimeSafari Daily Notification Plugin integration" + }, + endorser: { + baseUrl: "http://localhost:3001", + apiVersion: "v2", + endpoints: { + offers: "/api/v2/report/offers", + offersToPlans: "/api/v2/report/offersToPlansOwnedByMe", + plansLastUpdated: "/api/v2/report/plansLastUpdatedBetween", + notificationsBundle: "/api/v2/report/notifications/bundle" + }, + authentication: { + type: "Bearer", + token: "test-jwt-token-12345", + headers: { + "Authorization": "Bearer test-jwt-token-12345", + "Content-Type": "application/json", + "X-Privacy-Level": "user-controlled" + } + }, + pagination: { + defaultLimit: 50, + maxLimit: 100, + hitLimitThreshold: 50 + } + }, + notificationTypes: { + offers: { + enabled: true, + types: [ + "new_to_me", + "changed_to_me", + "new_to_projects", + "changed_to_projects", + "new_to_favorites", + "changed_to_favorites" + ] + }, + projects: { + enabled: true, + types: [ + "local_new", + "local_changed", + "content_interest_new", + "favorited_changed" + ] + }, + people: { + enabled: true, + types: [ + "local_new", + "local_changed", + "content_interest_new", + "favorited_changed", + "contacts_changed" + ] + }, + items: { + enabled: true, + types: [ + "local_new", + "local_changed", + "favorited_changed" + ] + } + }, + scheduling: { + contentFetch: { + schedule: "0 8 * * *", + time: "08:00", + description: "8 AM daily - fetch community updates" + }, + userNotification: { + schedule: "0 9 * * *", + time: "09:00", + description: "9 AM daily - notify users of community updates" + } + }, + testData: { + userDid: "did:example:testuser123", + starredPlanIds: [ + "plan-community-garden", + "plan-local-food", + "plan-sustainability" + ], + lastKnownOfferId: "01HSE3R9MAC0FT3P3KZ382TWV7", + lastKnownPlanId: "01HSE3R9MAC0FT3P3KZ382TWV8", + mockOffers: [ + { + jwtId: "01HSE3R9MAC0FT3P3KZ382TWV7", + handleId: "offer-web-dev-001", + offeredByDid: "did:example:offerer123", + recipientDid: "did:example:testuser123", + objectDescription: "Web development services for community project", + unit: "USD", + amount: 1000, + amountGiven: 500, + amountGivenConfirmed: 250 + } + ], + mockProjects: [ + { + plan: { + jwtId: "01HSE3R9MAC0FT3P3KZ382TWV8", + handleId: "plan-community-garden", + name: "Community Garden Project", + description: "Building a community garden for local food production", + issuerDid: "did:example:issuer123", + agentDid: "did:example:agent123" + }, + wrappedClaimBefore: null + } + ] + }, + callbacks: { + offers: { + enabled: true, + localHandler: "handleOffersNotification" + }, + projects: { + enabled: true, + localHandler: "handleProjectsNotification" + }, + people: { + enabled: true, + localHandler: "handlePeopleNotification" + }, + items: { + enabled: true, + localHandler: "handleItemsNotification" + }, + communityAnalytics: { + enabled: true, + endpoint: "http://localhost:3001/api/analytics/community-events", + headers: { + "Content-Type": "application/json", + "X-Privacy-Level": "aggregated" + } + } + }, + observability: { + enableLogging: true, + logLevel: "debug", + enableMetrics: true, + enableHealthChecks: true + } + }; + + return this.config; + } catch (error) { + console.error('Failed to load configuration:', error); + throw error; + } + } + + /** + * Get configuration + */ + public getConfig(): TimeSafariConfig { + if (!this.config) { + throw new Error('Configuration not loaded. Call loadConfig() first.'); + } + return this.config; + } + + /** + * Get Endorser.ch API URL for a specific endpoint + */ + public getEndorserUrl(endpoint: keyof TimeSafariConfig['endorser']['endpoints']): string { + const config = this.getConfig(); + return `${config.endorser.baseUrl}${config.endorser.endpoints[endpoint]}`; + } + + /** + * Get authentication headers + */ + public getAuthHeaders(): Record { + const config = this.getConfig(); + return config.endorser.authentication.headers; + } + + /** + * Get test data + */ + public getTestData() { + const config = this.getConfig(); + return config.testData; + } + + /** + * Get notification types for a specific category + */ + public getNotificationTypes(category: keyof TimeSafariConfig['notificationTypes']) { + const config = this.getConfig(); + return config.notificationTypes[category]; + } +} + +/** + * Logger utility for test apps + */ +export class TestLogger { + private logLevel: string; + + constructor(logLevel: string = 'debug') { + this.logLevel = logLevel; + } + + private shouldLog(level: string): boolean { + const levels = ['error', 'warn', 'info', 'debug']; + return levels.indexOf(level) <= levels.indexOf(this.logLevel); + } + + public debug(message: string, data?: any) { + if (this.shouldLog('debug')) { + console.log(`[DEBUG] ${message}`, data || ''); + } + } + + public info(message: string, data?: any) { + if (this.shouldLog('info')) { + console.log(`[INFO] ${message}`, data || ''); + } + } + + public warn(message: string, data?: any) { + if (this.shouldLog('warn')) { + console.warn(`[WARN] ${message}`, data || ''); + } + } + + public error(message: string, data?: any) { + if (this.shouldLog('error')) { + console.error(`[ERROR] ${message}`, data || ''); + } + } +} + +/** + * Mock DailyNotificationService for test apps + */ +export class MockDailyNotificationService { + private config: TimeSafariConfig; + private logger: TestLogger; + private isInitialized = false; + + constructor(config: TimeSafariConfig) { + this.config = config; + this.logger = new TestLogger(config.observability.logLevel); + } + + /** + * Initialize the service + */ + public async initialize(): Promise { + this.logger.info('Initializing Mock DailyNotificationService'); + this.isInitialized = true; + this.logger.info('Mock DailyNotificationService initialized successfully'); + } + + /** + * Schedule dual notification (content fetch + user notification) + */ + public async scheduleDualNotification(config: any): Promise { + if (!this.isInitialized) { + throw new Error('Service not initialized'); + } + + this.logger.info('Scheduling dual notification', config); + + // Simulate content fetch + if (config.contentFetch?.enabled) { + await this.simulateContentFetch(config.contentFetch); + } + + // Simulate user notification + if (config.userNotification?.enabled) { + await this.simulateUserNotification(config.userNotification); + } + + this.logger.info('Dual notification scheduled successfully'); + } + + /** + * Register callback + */ + public async registerCallback(name: string, callback: Function): Promise { + this.logger.info(`Registering callback: ${name}`); + // In a real implementation, this would register the callback + this.logger.info(`Callback ${name} registered successfully`); + } + + /** + * Get dual schedule status + */ + public async getDualScheduleStatus(): Promise { + return { + contentFetch: { + enabled: true, + lastFetch: new Date().toISOString(), + nextFetch: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString() + }, + userNotification: { + enabled: true, + lastNotification: new Date().toISOString(), + nextNotification: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString() + } + }; + } + + /** + * Cancel all notifications + */ + public async cancelAllNotifications(): Promise { + this.logger.info('Cancelling all notifications'); + this.logger.info('All notifications cancelled successfully'); + } + + /** + * Simulate content fetch using Endorser.ch API patterns + */ + private async simulateContentFetch(config: any): Promise { + this.logger.info('Simulating content fetch from Endorser.ch API'); + + try { + // Simulate parallel API requests + const testData = this.config.testData; + + // Mock offers to person + const offersToPerson = { + data: testData.mockOffers, + hitLimit: false + }; + + // Mock offers to projects + const offersToProjects = { + data: [], + hitLimit: false + }; + + // Mock starred project changes + const starredChanges = { + data: testData.mockProjects, + hitLimit: false + }; + + this.logger.info('Content fetch simulation completed', { + offersToPerson: offersToPerson.data.length, + offersToProjects: offersToProjects.data.length, + starredChanges: starredChanges.data.length + }); + + // Call success callback if provided + if (config.callbacks?.onSuccess) { + await config.callbacks.onSuccess({ + offersToPerson, + offersToProjects, + starredChanges + }); + } + } catch (error) { + this.logger.error('Content fetch simulation failed', error); + if (config.callbacks?.onError) { + await config.callbacks.onError(error); + } + } + } + + /** + * Simulate user notification + */ + private async simulateUserNotification(config: any): Promise { + this.logger.info('Simulating user notification', { + title: config.title, + body: config.body, + time: config.schedule + }); + this.logger.info('User notification simulation completed'); + } +} diff --git a/test-apps/test-api/server.js b/test-apps/test-api/server.js index ac3d6c1..0861339 100644 --- a/test-apps/test-api/server.js +++ b/test-apps/test-api/server.js @@ -1,13 +1,13 @@ #!/usr/bin/env node /** - * Test API Server for Daily Notification Plugin + * Test API Server for TimeSafari Daily Notification Plugin * - * Provides mock content endpoints for testing the plugin's - * network fetching, ETag support, and error handling capabilities. + * Simulates Endorser.ch API endpoints for testing the plugin's + * network fetching, pagination, and TimeSafari-specific functionality. * * @author Matthew Raymer - * @version 1.0.0 + * @version 2.0.0 */ const express = require('express'); @@ -24,67 +24,110 @@ app.use(express.json()); // In-memory storage for testing let contentStore = new Map(); let etagStore = new Map(); +let offersStore = new Map(); +let projectsStore = new Map(); /** - * Generate mock notification content for a given slot - * @param {string} slotId - The notification slot identifier - * @param {number} timestamp - Current timestamp - * @returns {Object} Mock notification content + * Generate mock offer data for TimeSafari testing + * @param {string} recipientDid - DID of the recipient + * @param {string} afterId - JWT ID for pagination + * @returns {Object} Mock offer data */ -function generateMockContent(slotId, timestamp) { - const slotTime = slotId.split('-')[1] || '08:00'; - const contentId = crypto.randomUUID().substring(0, 8); +function generateMockOffers(recipientDid, afterId) { + const offers = []; + const offerCount = Math.floor(Math.random() * 5) + 1; // 1-5 offers + + for (let i = 0; i < offerCount; i++) { + const jwtId = `01HSE3R9MAC0FT3P3KZ382TWV${7 + i}`; + const handleId = `offer-${crypto.randomUUID().substring(0, 8)}`; + + offers.push({ + jwtId: jwtId, + handleId: handleId, + issuedAt: new Date().toISOString(), + offeredByDid: `did:example:offerer${i + 1}`, + recipientDid: recipientDid, + unit: 'USD', + amount: Math.floor(Math.random() * 5000) + 500, + amountGiven: Math.floor(Math.random() * 2000) + 200, + amountGivenConfirmed: Math.floor(Math.random() * 1000) + 100, + objectDescription: `Community service offer ${i + 1}`, + validThrough: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), + fullClaim: { + type: 'Offer', + issuer: `did:example:offerer${i + 1}`, + recipient: recipientDid, + object: { + description: `Community service offer ${i + 1}`, + amount: Math.floor(Math.random() * 5000) + 500, + unit: 'USD' + } + } + }); + } return { - id: contentId, - slotId: slotId, - title: `Daily Update - ${slotTime}`, - body: `Your personalized content for ${slotTime}. Content ID: ${contentId}`, - timestamp: timestamp, - priority: 'high', - category: 'daily', - actions: [ - { id: 'view', title: 'View Details' }, - { id: 'dismiss', title: 'Dismiss' } - ], - metadata: { - source: 'test-api', - version: '1.0.0', - generated: new Date(timestamp).toISOString() - } + data: offers, + hitLimit: offers.length >= 3 // Simulate hit limit }; } /** - * Generate ETag for content - * @param {Object} content - Content object - * @returns {string} ETag value + * Generate mock project data for TimeSafari testing + * @param {Array} planIds - Array of plan IDs + * @param {string} afterId - JWT ID for pagination + * @returns {Object} Mock project data */ -function generateETag(content) { - const contentString = JSON.stringify(content); - return `"${crypto.createHash('md5').update(contentString).digest('hex')}"`; -} - -/** - * Store content with ETag - * @param {string} slotId - Slot identifier - * @param {Object} content - Content object - * @param {string} etag - ETag value - */ -function storeContent(slotId, content, etag) { - contentStore.set(slotId, content); - etagStore.set(slotId, etag); +function generateMockProjects(planIds, afterId) { + const projects = []; + + planIds.forEach((planId, index) => { + const jwtId = `01HSE3R9MAC0FT3P3KZ382TWV${8 + index}`; + + projects.push({ + plan: { + jwtId: jwtId, + handleId: planId, + name: `Community Project ${index + 1}`, + description: `Description for ${planId}`, + issuerDid: `did:example:issuer${index + 1}`, + agentDid: `did:example:agent${index + 1}`, + startTime: new Date().toISOString(), + endTime: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString(), + locLat: 40.7128 + (Math.random() - 0.5) * 0.1, + locLon: -74.0060 + (Math.random() - 0.5) * 0.1, + url: `https://timesafari.com/projects/${planId}`, + category: 'community', + status: 'active' + }, + wrappedClaimBefore: null // Simulate no previous claim + }); + }); + + return { + data: projects, + hitLimit: projects.length >= 2 // Simulate hit limit + }; } /** - * Get stored content and ETag - * @param {string} slotId - Slot identifier - * @returns {Object} { content, etag } or null + * Generate mock notification bundle for TimeSafari + * @param {Object} params - Request parameters + * @returns {Object} Mock notification bundle */ -function getStoredContent(slotId) { - const content = contentStore.get(slotId); - const etag = etagStore.get(slotId); - return content && etag ? { content, etag } : null; +function generateNotificationBundle(params) { + const { userDid, starredPlanIds, lastKnownOfferId, lastKnownPlanId } = params; + + return { + offersToPerson: generateMockOffers(userDid, lastKnownOfferId), + offersToProjects: { + data: [], + hitLimit: false + }, + starredChanges: generateMockProjects(starredPlanIds, lastKnownPlanId), + timestamp: new Date().toISOString(), + bundleId: crypto.randomUUID() + }; } // Routes @@ -96,19 +139,125 @@ app.get('/health', (req, res) => { res.json({ status: 'healthy', timestamp: Date.now(), - version: '1.0.0', + version: '2.0.0', + service: 'TimeSafari Test API', endpoints: { - content: '/api/content/:slotId', health: '/health', - metrics: '/api/metrics', - error: '/api/error/:type' + offers: '/api/v2/report/offers', + offersToPlans: '/api/v2/report/offersToPlansOwnedByMe', + plansLastUpdated: '/api/v2/report/plansLastUpdatedBetween', + notificationsBundle: '/api/v2/report/notifications/bundle', + analytics: '/api/analytics/community-events', + metrics: '/api/metrics' } }); }); /** - * Get notification content for a specific slot - * Supports ETag conditional requests + * Endorser.ch API: Get offers to person + */ +app.get('/api/v2/report/offers', (req, res) => { + const { recipientId, afterId } = req.query; + + console.log(`[${new Date().toISOString()}] GET /api/v2/report/offers`); + console.log(` recipientId: ${recipientId}, afterId: ${afterId || 'none'}`); + + if (!recipientId) { + return res.status(400).json({ + error: 'recipientId parameter is required' + }); + } + + const offers = generateMockOffers(recipientId, afterId); + + console.log(` → 200 OK (${offers.data.length} offers, hitLimit: ${offers.hitLimit})`); + res.json(offers); +}); + +/** + * Endorser.ch API: Get offers to user's projects + */ +app.get('/api/v2/report/offersToPlansOwnedByMe', (req, res) => { + const { afterId } = req.query; + + console.log(`[${new Date().toISOString()}] GET /api/v2/report/offersToPlansOwnedByMe`); + console.log(` afterId: ${afterId || 'none'}`); + + const offers = { + data: [], // Simulate no offers to user's projects + hitLimit: false + }; + + console.log(` → 200 OK (${offers.data.length} offers, hitLimit: ${offers.hitLimit})`); + res.json(offers); +}); + +/** + * Endorser.ch API: Get changes to starred projects + */ +app.post('/api/v2/report/plansLastUpdatedBetween', (req, res) => { + const { planIds, afterId } = req.body; + + console.log(`[${new Date().toISOString()}] POST /api/v2/report/plansLastUpdatedBetween`); + console.log(` planIds: ${JSON.stringify(planIds)}, afterId: ${afterId || 'none'}`); + + if (!planIds || !Array.isArray(planIds)) { + return res.status(400).json({ + error: 'planIds array is required' + }); + } + + const projects = generateMockProjects(planIds, afterId); + + console.log(` → 200 OK (${projects.data.length} projects, hitLimit: ${projects.hitLimit})`); + res.json(projects); +}); + +/** + * TimeSafari API: Get notification bundle + */ +app.get('/api/v2/report/notifications/bundle', (req, res) => { + const { userDid, starredPlanIds, lastKnownOfferId, lastKnownPlanId } = req.query; + + console.log(`[${new Date().toISOString()}] GET /api/v2/report/notifications/bundle`); + console.log(` userDid: ${userDid}, starredPlanIds: ${starredPlanIds}`); + + if (!userDid) { + return res.status(400).json({ + error: 'userDid parameter is required' + }); + } + + const bundle = generateNotificationBundle({ + userDid, + starredPlanIds: starredPlanIds ? JSON.parse(starredPlanIds) : [], + lastKnownOfferId, + lastKnownPlanId + }); + + console.log(` → 200 OK (bundle generated)`); + res.json(bundle); +}); + +/** + * TimeSafari Analytics: Community events + */ +app.post('/api/analytics/community-events', (req, res) => { + const { client_id, events } = req.body; + + console.log(`[${new Date().toISOString()}] POST /api/analytics/community-events`); + console.log(` client_id: ${client_id}, events: ${events?.length || 0}`); + + // Simulate analytics processing + res.json({ + status: 'success', + processed: events?.length || 0, + timestamp: new Date().toISOString() + }); +}); + +/** + * Legacy content endpoint (for backward compatibility) */ app.get('/api/content/:slotId', (req, res) => { const { slotId } = req.params; @@ -127,91 +276,61 @@ app.get('/api/content/:slotId', (req, res) => { } // Check if we have stored content - const stored = getStoredContent(slotId); + const stored = contentStore.get(slotId); + const etag = etagStore.get(slotId); - if (stored && ifNoneMatch === stored.etag) { + if (stored && etag && ifNoneMatch === etag) { // Content hasn't changed, return 304 Not Modified console.log(` → 304 Not Modified (ETag match)`); return res.status(304).end(); } // Generate new content - const content = generateMockContent(slotId, timestamp); - const etag = generateETag(content); + const content = { + id: crypto.randomUUID().substring(0, 8), + slotId: slotId, + title: `TimeSafari Community Update - ${slotId.split('-')[1]}`, + body: `Your personalized TimeSafari content for ${slotId.split('-')[1]}`, + timestamp: timestamp, + priority: 'high', + category: 'community', + 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' } + ], + metadata: { + source: 'timesafari-test-api', + version: '2.0.0', + generated: new Date(timestamp).toISOString() + } + }; + + const newEtag = `"${crypto.createHash('md5').update(JSON.stringify(content)).digest('hex')}"`; // Store for future ETag checks - storeContent(slotId, content, etag); + contentStore.set(slotId, content); + etagStore.set(slotId, newEtag); // Set ETag header - res.set('ETag', etag); + res.set('ETag', newEtag); res.set('Cache-Control', 'no-cache'); res.set('Last-Modified', new Date(timestamp).toUTCString()); - console.log(` → 200 OK (new content, ETag: ${etag})`); + console.log(` → 200 OK (new content, ETag: ${newEtag})`); res.json(content); }); -/** - * Simulate network errors for testing error handling - */ -app.get('/api/error/:type', (req, res) => { - const { type } = req.params; - - console.log(`[${new Date().toISOString()}] GET /api/error/${type}`); - - switch (type) { - case 'timeout': - // Simulate timeout by not responding - setTimeout(() => { - res.status(408).json({ error: 'Request timeout' }); - }, 15000); // 15 second timeout - break; - - case 'server-error': - res.status(500).json({ - error: 'Internal server error', - code: 'INTERNAL_ERROR', - timestamp: Date.now() - }); - break; - - case 'not-found': - res.status(404).json({ - error: 'Content not found', - code: 'NOT_FOUND', - slotId: req.query.slotId || 'unknown' - }); - break; - - case 'rate-limit': - res.status(429).json({ - error: 'Rate limit exceeded', - code: 'RATE_LIMIT', - retryAfter: 60 - }); - break; - - case 'unauthorized': - res.status(401).json({ - error: 'Unauthorized', - code: 'UNAUTHORIZED' - }); - break; - - default: - res.status(400).json({ - error: 'Unknown error type', - available: ['timeout', 'server-error', 'not-found', 'rate-limit', 'unauthorized'] - }); - } -}); - /** * API metrics endpoint */ app.get('/api/metrics', (req, res) => { const metrics = { timestamp: Date.now(), + service: 'TimeSafari Test API', + version: '2.0.0', contentStore: { size: contentStore.size, slots: Array.from(contentStore.keys()) @@ -221,7 +340,21 @@ app.get('/api/metrics', (req, res) => { etags: Array.from(etagStore.entries()) }, uptime: process.uptime(), - memory: process.memoryUsage() + memory: process.memoryUsage(), + endpoints: { + total: 8, + active: 8, + health: '/health', + endorser: { + offers: '/api/v2/report/offers', + offersToPlans: '/api/v2/report/offersToPlansOwnedByMe', + plansLastUpdated: '/api/v2/report/plansLastUpdatedBetween' + }, + timesafari: { + notificationsBundle: '/api/v2/report/notifications/bundle', + analytics: '/api/analytics/community-events' + } + } }; res.json(metrics); @@ -240,33 +373,6 @@ app.delete('/api/content', (req, res) => { }); }); -/** - * Update content for a specific slot (for testing content changes) - */ -app.put('/api/content/:slotId', (req, res) => { - const { slotId } = req.params; - const { content } = req.body; - - if (!content) { - return res.status(400).json({ - error: 'Content is required' - }); - } - - const timestamp = Date.now(); - const etag = generateETag(content); - - storeContent(slotId, content, etag); - - res.set('ETag', etag); - res.json({ - message: 'Content updated', - slotId, - etag, - timestamp - }); -}); - // Error handling middleware app.use((err, req, res, next) => { console.error(`[${new Date().toISOString()}] Error:`, err); @@ -289,14 +395,16 @@ app.use((req, res) => { // Start server app.listen(PORT, () => { - console.log(`🚀 Test API Server running on port ${PORT}`); + console.log(`🚀 TimeSafari Test API Server running on port ${PORT}`); console.log(`📋 Available endpoints:`); - console.log(` GET /health - Health check`); - console.log(` GET /api/content/:slotId - Get notification content`); - console.log(` PUT /api/content/:slotId - Update content`); - console.log(` DELETE /api/content - Clear all content`); - console.log(` GET /api/error/:type - Simulate errors`); - console.log(` GET /api/metrics - API metrics`); + console.log(` GET /health - Health check`); + console.log(` GET /api/v2/report/offers - Get offers to person`); + console.log(` GET /api/v2/report/offersToPlansOwnedByMe - Get offers to user's projects`); + console.log(` POST /api/v2/report/plansLastUpdatedBetween - Get changes to starred projects`); + console.log(` GET /api/v2/report/notifications/bundle - Get TimeSafari notification bundle`); + console.log(` POST /api/analytics/community-events - Send community analytics`); + console.log(` GET /api/content/:slotId - Legacy content endpoint`); + console.log(` GET /api/metrics - API metrics`); console.log(``); console.log(`🔧 Environment:`); console.log(` NODE_ENV: ${process.env.NODE_ENV || 'development'}`); @@ -304,18 +412,18 @@ app.listen(PORT, () => { console.log(``); console.log(`📝 Usage examples:`); console.log(` curl http://localhost:${PORT}/health`); - console.log(` curl http://localhost:${PORT}/api/content/slot-08:00`); - console.log(` curl -H "If-None-Match: \\"abc123\\"" http://localhost:${PORT}/api/content/slot-08:00`); - console.log(` curl http://localhost:${PORT}/api/error/timeout`); + console.log(` curl "http://localhost:${PORT}/api/v2/report/offers?recipientId=did:example:testuser123&afterId=01HSE3R9MAC0FT3P3KZ382TWV7"`); + console.log(` curl -X POST http://localhost:${PORT}/api/v2/report/plansLastUpdatedBetween -H "Content-Type: application/json" -d '{"planIds":["plan-123","plan-456"],"afterId":"01HSE3R9MAC0FT3P3KZ382TWV8"}'`); + console.log(` curl "http://localhost:${PORT}/api/v2/report/notifications/bundle?userDid=did:example:testuser123&starredPlanIds=[\"plan-123\",\"plan-456\"]"`); }); // Graceful shutdown process.on('SIGINT', () => { - console.log('\n🛑 Shutting down Test API Server...'); + console.log('\n🛑 Shutting down TimeSafari Test API Server...'); process.exit(0); }); process.on('SIGTERM', () => { - console.log('\n🛑 Shutting down Test API Server...'); + console.log('\n🛑 Shutting down TimeSafari Test API Server...'); process.exit(0); });