# TimeSafari PWA - CapacitorPlatformService Clean Changes **Author**: Matthew Raymer **Version**: 1.0.0 **Created**: 2025-10-08 06:24:57 UTC ## Overview This document shows the **exact changes** needed to the existing TimeSafari PWA `CapacitorPlatformService` to add DailyNotification plugin functionality. The plugin code **ONLY touches Capacitor classes** - no `isCapacitor` flags needed. ## Required Changes to Existing TimeSafari PWA Code ### File: `src/services/platforms/CapacitorPlatformService.ts` #### 1. Add New Imports (at the top of the file) ```typescript // ADD THESE IMPORTS import { DailyNotification } from '@timesafari/daily-notification-plugin'; import { TimeSafariIntegrationService } from '@timesafari/daily-notification-plugin'; ``` #### 2. Add New Interfaces (after existing interfaces) ```typescript // ADD THESE INTERFACES interface PlanSummaryAndPreviousClaim { id: string; title: string; description: string; lastUpdated: string; previousClaim?: unknown; } interface StarredProjectsResponse { data: Array; hitLimit: boolean; } interface TimeSafariSettings { accountDid?: string; activeDid?: string; apiServer?: string; starredPlanHandleIds?: string[]; lastAckedStarredPlanChangesJwtId?: string; [key: string]: unknown; } ``` #### 3. Add New Properties (in the class, after existing properties) ```typescript export class CapacitorPlatformService implements PlatformService { // ... existing properties ... // ADD THESE NEW PROPERTIES private dailyNotificationService: DailyNotification | null = null; private integrationService: TimeSafariIntegrationService | null = null; private dailyNotificationInitialized = false; // ActiveDid change tracking private currentActiveDid: string | null = null; } ``` #### 4. Modify Existing updateActiveDid Method ```typescript async updateActiveDid(did: string): Promise { const oldDid = this.currentActiveDid; // Update the database (existing TimeSafari pattern) await this.dbExec( "UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1", [did], ); // Update local tracking this.currentActiveDid = did; // Update DailyNotification plugin if initialized if (this.dailyNotificationInitialized) { await this.updateDailyNotificationActiveDid(did, oldDid); } logger.debug( `[CapacitorPlatformService] ActiveDid updated from ${oldDid} to ${did}` ); } ``` #### 5. Modify Existing initializeDatabase Method ```typescript private async initializeDatabase(): Promise { // If already initialized, return immediately if (this.initialized) { return; } // If initialization is in progress, wait for it if (this.initializationPromise) { return this.initializationPromise; } try { // Start initialization this.initializationPromise = this._initialize(); await this.initializationPromise; // ADD THIS LINE: Initialize DailyNotification after database is ready await this.initializeDailyNotification(); } catch (error) { logger.error( "[CapacitorPlatformService] Initialize database method failed:", error, ); this.initializationPromise = null; // Reset on failure throw error; } } ``` #### 6. Add New Methods (in the class, after existing methods) ```typescript /** * Initialize DailyNotification plugin with TimeSafari configuration */ async initializeDailyNotification(): Promise { if (this.dailyNotificationInitialized) { return; } try { logger.log("[CapacitorPlatformService] Initializing DailyNotification plugin..."); // Get current TimeSafari settings const settings = await this.getTimeSafariSettings(); // Get current activeDid const currentActiveDid = await this.getCurrentActiveDid(); // 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 current activeDid activeDid: currentActiveDid || '', // 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, // Special retry for activeDid changes starredProjectsRetries: 3 } }, // Network configuration using existing TimeSafari patterns networkConfig: { 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) } } }); // Initialize TimeSafari Integration Service this.integrationService = TimeSafariIntegrationService.getInstance(); await this.integrationService.initialize({ activeDid: currentActiveDid || '', 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.dailyNotificationInitialized = true; this.currentActiveDid = currentActiveDid; logger.log("[CapacitorPlatformService] DailyNotification plugin initialized successfully"); } catch (error) { logger.error("[CapacitorPlatformService] Failed to initialize DailyNotification plugin:", error); throw error; } } /** * Enhanced version of existing TimeSafari loadNewStarredProjectChanges method */ async loadNewStarredProjectChanges(): Promise { // Ensure DailyNotification is initialized if (!this.dailyNotificationInitialized) { await this.initializeDailyNotification(); } const settings = await this.getTimeSafariSettings(); const currentActiveDid = await this.getCurrentActiveDid(); if (!currentActiveDid || !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( currentActiveDid, settings.starredPlanHandleIds, settings.lastAckedStarredPlanChangesJwtId ); // Enhanced logging (optional) logger.log("[CapacitorPlatformService] Starred projects loaded successfully:", { count: starredProjectChanges.data.length, hitLimit: starredProjectChanges.hitLimit, planIds: settings.starredPlanHandleIds.length, activeDid: currentActiveDid }); return starredProjectChanges; } catch (error) { // Same error handling as existing TimeSafari code logger.warn("[CapacitorPlatformService] Failed to load starred project changes:", error); return { data: [], hitLimit: false }; } } /** * Update DailyNotification plugin when activeDid changes */ private async updateDailyNotificationActiveDid(newDid: string, oldDid: string | null): Promise { try { logger.log(`[CapacitorPlatformService] Updating DailyNotification plugin activeDid from ${oldDid} to ${newDid}`); // Get new settings for the new activeDid const newSettings = await this.getTimeSafariSettings(); // Reconfigure DailyNotification plugin with new activeDid await DailyNotification.configure({ timesafariConfig: { activeDid: newDid, endpoints: { offersToPerson: `${newSettings.apiServer}/api/v2/offers/person`, offersToPlans: `${newSettings.apiServer}/api/v2/offers/plans`, projectsLastUpdated: `${newSettings.apiServer}/api/v2/report/plansLastUpdatedBetween` }, starredProjectsConfig: { enabled: true, starredPlanHandleIds: newSettings.starredPlanHandleIds || [], lastAckedJwtId: newSettings.lastAckedStarredPlanChangesJwtId || '', fetchInterval: '0 8 * * *' } } }); // Update TimeSafari Integration Service if (this.integrationService) { await this.integrationService.initialize({ activeDid: newDid, storageAdapter: this.getTimeSafariStorageAdapter(), endorserApiBaseUrl: newSettings.apiServer || 'https://endorser.ch' }); } logger.log(`[CapacitorPlatformService] DailyNotification plugin updated successfully for activeDid: ${newDid}`); } catch (error) { logger.error(`[CapacitorPlatformService] Failed to update DailyNotification plugin activeDid:`, error); } } /** * Get current activeDid from the database */ private async getCurrentActiveDid(): Promise { try { const result = await this.dbQuery( "SELECT activeDid FROM active_identity WHERE id = 1" ); if (result?.values?.length) { const activeDid = result.values[0][0] as string | null; return activeDid; } return null; } catch (error) { logger.error("[CapacitorPlatformService] Error getting current activeDid:", error); return null; } } /** * Get TimeSafari settings using existing database patterns */ private async getTimeSafariSettings(): Promise { try { // Get current activeDid const currentActiveDid = await this.getCurrentActiveDid(); if (!currentActiveDid) { return {}; } // Use existing TimeSafari settings retrieval pattern const result = await this.dbQuery( "SELECT * FROM settings WHERE accountDid = ?", [currentActiveDid] ); if (!result?.values?.length) { return {}; } // Map database columns to values (existing TimeSafari pattern) const settings: TimeSafariSettings = {}; result.columns.forEach((column, index) => { if (column !== 'id') { settings[column] = result.values[0][index]; } }); // Set activeDid from current value settings.activeDid = currentActiveDid; // 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) { logger.error("[CapacitorPlatformService] 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.dbExec( "INSERT OR REPLACE INTO temp (id, data) VALUES (?, ?)", [key, JSON.stringify(value)] ); }, retrieve: async (key: string) => { const result = await this.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 handler for successful starred projects fetch */ private async handleStarredProjectsSuccess(data: StarredProjectsResponse): Promise { // Enhanced logging (optional) logger.log("[CapacitorPlatformService] Starred projects success callback:", { count: data.data.length, hitLimit: data.hitLimit, activeDid: this.currentActiveDid }); // Store results in TimeSafari temp table for UI access await this.dbExec( "INSERT OR REPLACE INTO temp (id, data) VALUES (?, ?)", ['starred_projects_latest', JSON.stringify(data)] ); } /** * Callback handler for starred projects fetch errors */ private async handleStarredProjectsError(error: Error): Promise { // Same error handling as existing TimeSafari code logger.warn("[CapacitorPlatformService] Failed to load starred project changes:", error); // Store error in TimeSafari temp table for UI access await this.dbExec( "INSERT OR REPLACE INTO temp (id, data) VALUES (?, ?)", ['starred_projects_error', JSON.stringify({ error: error.message, timestamp: Date.now(), activeDid: this.currentActiveDid })] ); } /** * Callback handler for starred projects fetch completion */ private async handleStarredProjectsComplete(result: unknown): Promise { // Handle completion logger.log("[CapacitorPlatformService] Starred projects fetch completed:", { result, activeDid: this.currentActiveDid }); } /** * Get DailyNotification plugin status for debugging */ async getDailyNotificationStatus(): Promise<{ initialized: boolean; platform: string; capabilities: PlatformCapabilities; currentActiveDid: string | null; }> { return { initialized: this.dailyNotificationInitialized, platform: Capacitor.getPlatform(), capabilities: this.getCapabilities(), currentActiveDid: this.currentActiveDid }; } ``` ## Package.json Changes ### Add DailyNotification Plugin Dependency ```json { "dependencies": { "@timesafari/daily-notification-plugin": "ssh://git@173.199.124.46:222/trent_larson/daily-notification-plugin.git" } } ``` ## Summary of Changes ### Files Modified: 1. **`src/services/platforms/CapacitorPlatformService.ts`** - Add imports for DailyNotification plugin - Add new interfaces for plugin integration - Add new properties for plugin state - Modify existing `updateActiveDid` method - Modify existing `initializeDatabase` method - Add new methods for plugin functionality 2. **`package.json`** - Add DailyNotification plugin dependency ### Key Benefits: - **Same Interface**: Existing methods work exactly the same - **Enhanced Functionality**: Background fetching, structured logging, error handling - **ActiveDid Change Handling**: Plugin automatically reconfigures when activeDid changes - **No Platform Flags**: Plugin code only touches Capacitor classes - **No Breaking Changes**: Existing code continues to work ### Migration Strategy: 1. **Add the changes** to existing TimeSafari PWA CapacitorPlatformService 2. **Test on Capacitor platforms** (Android, iOS) 3. **Verify activeDid changes** work correctly 4. **Gradually migrate** individual methods to use plugin features 5. **Leverage advanced features** like background fetching and observability --- **These are the exact changes needed to integrate the DailyNotification plugin with the existing TimeSafari PWA CapacitorPlatformService. The plugin code ONLY touches Capacitor classes - no `isCapacitor` flags needed.**