/** * TimeSafari PWA - DailyNotification Plugin Integration Example * * This example shows how to integrate the DailyNotification plugin with the existing * TimeSafari PWA architecture, specifically the CapacitorPlatformService and * PlatformServiceMixin patterns. * * @author Matthew Raymer * @version 1.0.0 */ import { DailyNotification } from '@timesafari/daily-notification-plugin'; import { TimeSafariIntegrationService } from '@timesafari/daily-notification-plugin'; import { PlatformServiceFactory } from '@/services/PlatformServiceFactory'; import { Capacitor } from '@capacitor/core'; // TimeSafari PWA existing interfaces (from the actual project) interface PlanSummaryAndPreviousClaim { id: string; title: string; description: string; lastUpdated: string; previousClaim?: unknown; } interface StarredProjectsResponse { data: Array; hitLimit: boolean; } interface Settings { accountDid?: string; activeDid?: string; apiServer?: string; starredPlanHandleIds?: string[]; lastAckedStarredPlanChangesJwtId?: string; [key: string]: unknown; } /** * Enhanced CapacitorPlatformService with DailyNotification integration * * This extends the existing TimeSafari PWA CapacitorPlatformService to include * DailyNotification plugin functionality while maintaining the same interface. */ export class EnhancedCapacitorPlatformService { private platformService = PlatformServiceFactory.getInstance(); private dailyNotificationService: DailyNotification | null = null; private integrationService: TimeSafariIntegrationService | null = null; private initialized = false; /** * Initialize DailyNotification plugin with TimeSafari PWA configuration */ async initializeDailyNotification(): Promise { if (this.initialized) { return; } try { // Get current TimeSafari settings const settings = await this.getTimeSafariSettings(); // Configure DailyNotification plugin with TimeSafari data await DailyNotification.configure({ // Basic plugin configuration storage: 'tiered', ttlSeconds: 1800, enableETagSupport: true, enableErrorHandling: true, enablePerformanceOptimization: true, // TimeSafari-specific configuration timesafariConfig: { // Use existing TimeSafari activeDid activeDid: settings.activeDid || '', // Use existing TimeSafari API endpoints endpoints: { offersToPerson: `${settings.apiServer}/api/v2/offers/person`, offersToPlans: `${settings.apiServer}/api/v2/offers/plans`, projectsLastUpdated: `${settings.apiServer}/api/v2/report/plansLastUpdatedBetween` }, // Configure starred projects fetching (matches existing TimeSafari pattern) starredProjectsConfig: { enabled: true, starredPlanHandleIds: settings.starredPlanHandleIds || [], lastAckedJwtId: settings.lastAckedStarredPlanChangesJwtId || '', fetchInterval: '0 8 * * *', // Daily at 8 AM maxResults: 50, hitLimitHandling: 'warn' // Same as existing TimeSafari error handling }, // Sync configuration (optimized for TimeSafari use case) syncConfig: { enableParallel: true, maxConcurrent: 3, batchSize: 10, timeout: 30000, retryAttempts: 3 }, // Error policy (matches existing TimeSafari error handling) errorPolicy: { maxRetries: 3, backoffMultiplier: 2, activeDidChangeRetries: 5, starredProjectsRetries: 3 } }, // Network configuration using existing TimeSafari patterns networkConfig: { // Use existing TimeSafari HTTP client (if available) baseURL: settings.apiServer || 'https://endorser.ch', timeout: 30000, retryAttempts: 3, retryDelay: 1000, maxConcurrent: 5, // Headers matching TimeSafari pattern defaultHeaders: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'User-Agent': 'TimeSafari-PWA/1.0.0' } }, // Content fetch configuration (replaces existing loadNewStarredProjectChanges) contentFetch: { enabled: true, schedule: '0 8 * * *', // Daily at 8 AM // Use existing TimeSafari request pattern requestConfig: { method: 'POST', url: `${settings.apiServer}/api/v2/report/plansLastUpdatedBetween`, headers: { 'Authorization': 'Bearer ${jwt}', 'X-User-DID': '${activeDid}', 'Content-Type': 'application/json' }, body: { planIds: '${starredPlanHandleIds}', afterId: '${lastAckedJwtId}' } }, // Callbacks that match TimeSafari error handling callbacks: { onSuccess: this.handleStarredProjectsSuccess.bind(this), onError: this.handleStarredProjectsError.bind(this), onComplete: this.handleStarredProjectsComplete.bind(this) } }, // Authentication configuration authentication: { jwt: { secret: process.env.JWT_SECRET || 'timesafari-jwt-secret', algorithm: 'HS256', expirationMinutes: 60, refreshThresholdMinutes: 10 } }, // Observability configuration logging: { level: 'INFO', enableRequestLogging: true, enableResponseLogging: true, enableErrorLogging: true, redactSensitiveData: true }, // Security configuration security: { certificatePinning: { enabled: true, pins: [ { hostname: 'endorser.ch', pins: ['sha256/YOUR_PIN_HERE'] } ] } } }); // Initialize TimeSafari Integration Service this.integrationService = TimeSafariIntegrationService.getInstance(); await this.integrationService.initialize({ activeDid: settings.activeDid || '', storageAdapter: this.getTimeSafariStorageAdapter(), endorserApiBaseUrl: settings.apiServer || 'https://endorser.ch', // Use existing TimeSafari request patterns requestConfig: { baseURL: settings.apiServer || 'https://endorser.ch', timeout: 30000, retryAttempts: 3 }, // Configure starred projects fetching starredProjectsConfig: { enabled: true, starredPlanHandleIds: settings.starredPlanHandleIds || [], lastAckedJwtId: settings.lastAckedStarredPlanChangesJwtId || '', fetchInterval: '0 8 * * *', maxResults: 50 } }); // Schedule daily notifications await DailyNotification.scheduleDailyNotification({ title: 'TimeSafari Community Update', body: 'You have new offers and project updates', time: '09:00', channel: 'timesafari_community_updates' }); this.initialized = true; console.log('DailyNotification plugin initialized successfully with TimeSafari PWA'); } catch (error) { console.error('Failed to initialize DailyNotification plugin:', error); throw error; } } /** * Enhanced version of existing TimeSafari loadNewStarredProjectChanges method * * This replaces the existing method with plugin-enhanced functionality * while maintaining the same interface and behavior. */ async loadNewStarredProjectChanges(): Promise { if (!this.initialized) { await this.initializeDailyNotification(); } const settings = await this.getTimeSafariSettings(); if (!settings.activeDid || !settings.starredPlanHandleIds?.length) { return { data: [], hitLimit: false }; } try { // Use plugin's enhanced fetching with same interface as existing TimeSafari code const starredProjectChanges = await this.integrationService!.getStarredProjectsWithChanges( settings.activeDid, settings.starredPlanHandleIds, settings.lastAckedStarredPlanChangesJwtId ); // Enhanced logging (optional) console.log('Starred projects loaded successfully:', { count: starredProjectChanges.data.length, hitLimit: starredProjectChanges.hitLimit, planIds: settings.starredPlanHandleIds.length }); return starredProjectChanges; } catch (error) { // Same error handling as existing TimeSafari code console.warn('[TimeSafari] Failed to load starred project changes:', error); return { data: [], hitLimit: false }; } } /** * Get TimeSafari settings using existing PlatformServiceMixin pattern */ private async getTimeSafariSettings(): Promise { try { // Use existing TimeSafari settings retrieval pattern const result = await this.platformService.dbQuery( "SELECT * FROM settings WHERE id = 1" ); if (!result?.values?.length) { return {}; } // Map database columns to values (existing TimeSafari pattern) const settings: Settings = {}; result.columns.forEach((column, index) => { if (column !== 'id') { settings[column] = result.values[0][index]; } }); // Handle JSON field parsing (existing TimeSafari pattern) if (settings.starredPlanHandleIds && typeof settings.starredPlanHandleIds === 'string') { try { settings.starredPlanHandleIds = JSON.parse(settings.starredPlanHandleIds); } catch { settings.starredPlanHandleIds = []; } } return settings; } catch (error) { console.error('Error getting TimeSafari settings:', error); return {}; } } /** * Get TimeSafari storage adapter using existing patterns */ private getTimeSafariStorageAdapter(): unknown { // Return existing TimeSafari storage adapter return { // Use existing TimeSafari storage patterns store: async (key: string, value: unknown) => { await this.platformService.dbExec( "INSERT OR REPLACE INTO temp (id, data) VALUES (?, ?)", [key, JSON.stringify(value)] ); }, retrieve: async (key: string) => { const result = await this.platformService.dbQuery( "SELECT data FROM temp WHERE id = ?", [key] ); if (result?.values?.length) { try { return JSON.parse(result.values[0][0] as string); } catch { return null; } } return null; } }; } /** * Callback handlers that match existing TimeSafari error handling patterns */ private async handleStarredProjectsSuccess(data: StarredProjectsResponse): Promise { // Enhanced logging (optional) console.log('Starred projects success callback:', { count: data.data.length, hitLimit: data.hitLimit }); // Store results in TimeSafari temp table for UI access await this.platformService.dbExec( "INSERT OR REPLACE INTO temp (id, data) VALUES (?, ?)", ['starred_projects_latest', JSON.stringify(data)] ); } private async handleStarredProjectsError(error: Error): Promise { // Same error handling as existing TimeSafari code console.warn('[TimeSafari] Failed to load starred project changes:', error); // Store error in TimeSafari temp table for UI access await this.platformService.dbExec( "INSERT OR REPLACE INTO temp (id, data) VALUES (?, ?)", ['starred_projects_error', JSON.stringify({ error: error.message, timestamp: Date.now() })] ); } private async handleStarredProjectsComplete(result: unknown): Promise { // Handle completion console.log('Starred projects fetch completed:', result); } /** * Get plugin status for debugging */ async getDailyNotificationStatus(): Promise<{ initialized: boolean; platform: string; capabilities: unknown; }> { return { initialized: this.initialized, platform: Capacitor.getPlatform(), capabilities: this.platformService.getCapabilities() }; } } /** * Vue.js Mixin for DailyNotification integration with TimeSafari PWA * * This mixin extends the existing PlatformServiceMixin to include * DailyNotification functionality while maintaining the same patterns. */ export const TimeSafariDailyNotificationMixin = { data() { return { // Existing TimeSafari data activeDid: '', starredPlanHandleIds: [] as string[], lastAckedStarredPlanChangesJwtId: '', numNewStarredProjectChanges: 0, newStarredProjectChangesHitLimit: false, // Plugin integration dailyNotificationService: null as DailyNotification | null, integrationService: null as TimeSafariIntegrationService | null, enhancedPlatformService: null as EnhancedCapacitorPlatformService | null }; }, computed: { // Existing TimeSafari computed properties isCapacitor(): boolean { return this.platformService.isCapacitor(); }, isWeb(): boolean { return this.platformService.isWeb(); }, isElectron(): boolean { return this.platformService.isElectron(); } }, async mounted() { // Initialize DailyNotification when component mounts (only on Capacitor) if (this.isCapacitor) { await this.initializeDailyNotification(); } }, methods: { /** * Initialize DailyNotification plugin with TimeSafari configuration */ async initializeDailyNotification(): Promise { try { // Create enhanced platform service this.enhancedPlatformService = new EnhancedCapacitorPlatformService(); // Initialize the plugin await this.enhancedPlatformService.initializeDailyNotification(); console.log('DailyNotification initialized successfully in TimeSafari PWA'); } catch (error) { console.error('Failed to initialize DailyNotification in TimeSafari PWA:', error); } }, /** * Enhanced version of existing TimeSafari loadNewStarredProjectChanges method */ async loadNewStarredProjectChanges(): Promise { if (this.isCapacitor && this.enhancedPlatformService) { // Use plugin-enhanced method on Capacitor const result = await this.enhancedPlatformService.loadNewStarredProjectChanges(); this.numNewStarredProjectChanges = result.data.length; this.newStarredProjectChangesHitLimit = result.hitLimit; } else { // Use existing web method in browser await this.loadNewStarredProjectChangesWeb(); } }, /** * Existing web-only method (unchanged) */ async loadNewStarredProjectChangesWeb(): Promise { // Your existing web-only implementation console.log('Using web-only method for starred projects'); }, /** * Get DailyNotification status for debugging */ async getDailyNotificationStatus(): Promise { if (this.enhancedPlatformService) { return await this.enhancedPlatformService.getDailyNotificationStatus(); } return { initialized: false, platform: 'web' }; } } }; /** * Usage example in a TimeSafari PWA Vue component */ export const TimeSafariHomeViewExample = { name: 'TimeSafariHomeView', mixins: [TimeSafariDailyNotificationMixin], data() { return { // Existing TimeSafari data activeDid: '', starredPlanHandleIds: [] as string[], lastAckedStarredPlanChangesJwtId: '', numNewStarredProjectChanges: 0, newStarredProjectChangesHitLimit: false }; }, async mounted() { // Load existing TimeSafari data await this.loadTimeSafariData(); // Initialize DailyNotification (only on Capacitor) if (this.isCapacitor) { await this.initializeDailyNotification(); } }, methods: { /** * Load existing TimeSafari data using PlatformServiceMixin */ async loadTimeSafariData(): Promise { try { // Use existing TimeSafari settings pattern const settings = await this.$settings(); this.activeDid = settings.activeDid || ''; this.starredPlanHandleIds = settings.starredPlanHandleIds || []; this.lastAckedStarredPlanChangesJwtId = settings.lastAckedStarredPlanChangesJwtId || ''; } catch (error) { console.error('Error loading TimeSafari data:', error); } }, /** * Enhanced loadNewStarredProjectChanges method */ async loadNewStarredProjectChanges(): Promise { if (this.isCapacitor && this.enhancedPlatformService) { // Use plugin-enhanced method on Capacitor const result = await this.enhancedPlatformService.loadNewStarredProjectChanges(); this.numNewStarredProjectChanges = result.data.length; this.newStarredProjectChangesHitLimit = result.hitLimit; } else { // Use existing web method in browser await this.loadNewStarredProjectChangesWeb(); } }, /** * Existing web-only method (unchanged) */ async loadNewStarredProjectChangesWeb(): Promise { // Your existing web-only implementation console.log('Using web-only method for starred projects'); } } }; /** * Factory function to create enhanced platform service */ export function createEnhancedPlatformService(): EnhancedCapacitorPlatformService { return new EnhancedCapacitorPlatformService(); } /** * Utility function to check if DailyNotification should be used */ export function shouldUseDailyNotification(): boolean { return Capacitor.isNativePlatform(); } /** * Utility function to get platform-specific configuration */ export function getPlatformConfig(): { usePlugin: boolean; platform: string; capabilities: unknown; } { const platform = Capacitor.getPlatform(); const platformService = PlatformServiceFactory.getInstance(); return { usePlugin: Capacitor.isNativePlatform(), platform, capabilities: platformService.getCapabilities() }; }