/** * TimeSafari PWA - CapacitorPlatformService ActiveDid Integration Example * * This example shows how to extend the existing CapacitorPlatformService * to handle activeDid changes and integrate with the DailyNotification plugin. * * This represents the ACTUAL CHANGES needed to handle activeDid changes * in the existing TimeSafari PWA CapacitorPlatformService. * * @author Matthew Raymer * @version 1.0.0 */ // ================================================= // EXISTING TIMESAFARI PWA CODE (unchanged) // ================================================= import { Filesystem, Directory, Encoding } from "@capacitor/filesystem"; import { Camera, CameraResultType, CameraSource, CameraDirection, } from "@capacitor/camera"; import { Capacitor } from "@capacitor/core"; import { Share } from "@capacitor/share"; import { SQLiteConnection, SQLiteDBConnection, CapacitorSQLite, DBSQLiteValues, } from "@capacitor-community/sqlite"; import { runMigrations } from "@/db-sql/migration"; import { QueryExecResult } from "@/interfaces/database"; import { ImageResult, PlatformService, PlatformCapabilities, } from "../PlatformService"; import { logger } from "../../utils/logger"; // ================================================= // NEW IMPORTS FOR DAILYNOTIFICATION PLUGIN // ================================================= import { DailyNotification } from '@timesafari/daily-notification-plugin'; import { TimeSafariIntegrationService } from '@timesafari/daily-notification-plugin'; // ================================================= // EXISTING INTERFACES (unchanged) // ================================================= interface QueuedOperation { type: "run" | "query" | "rawQuery"; sql: string; params: unknown[]; resolve: (value: unknown) => void; reject: (reason: unknown) => void; } // ================================================= // NEW INTERFACES FOR DAILYNOTIFICATION PLUGIN // ================================================= 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; } /** * EXTENDED CapacitorPlatformService with DailyNotification and ActiveDid Integration * * This shows the ACTUAL CHANGES needed to the existing TimeSafari PWA * CapacitorPlatformService class to handle activeDid changes and integrate * with the DailyNotification plugin. */ export class CapacitorPlatformService implements PlatformService { // ================================================= // EXISTING PROPERTIES (unchanged) // ================================================= /** Current camera direction */ private currentDirection: CameraDirection = CameraDirection.Rear; private sqlite: SQLiteConnection; private db: SQLiteDBConnection | null = null; private dbName = "timesafari.sqlite"; private initialized = false; private initializationPromise: Promise | null = null; private operationQueue: Array = []; private isProcessingQueue: boolean = false; // ================================================= // NEW PROPERTIES FOR DAILYNOTIFICATION PLUGIN // ================================================= private dailyNotificationService: DailyNotification | null = null; private integrationService: TimeSafariIntegrationService | null = null; private dailyNotificationInitialized = false; // ActiveDid change tracking private currentActiveDid: string | null = null; private activeDidChangeListeners: Array<(newDid: string | null, oldDid: string | null) => void> = []; // ================================================= // EXISTING CONSTRUCTOR (unchanged) // ================================================= constructor() { this.sqlite = new SQLiteConnection(CapacitorSQLite); } // ================================================= // MODIFIED METHOD: Enhanced updateActiveDid with DailyNotification Integration // ================================================= /** * Enhanced updateActiveDid method that handles DailyNotification plugin updates * * This method extends the existing updateActiveDid method to also update * the DailyNotification plugin when the activeDid changes. */ 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; // Notify listeners of the change this.notifyActiveDidChange(did, oldDid); // Update DailyNotification plugin if initialized if (this.dailyNotificationInitialized) { await this.updateDailyNotificationActiveDid(did, oldDid); } logger.debug( `[CapacitorPlatformService] ActiveDid updated from ${oldDid} to ${did}` ); } // ================================================= // NEW METHOD: Update DailyNotification Plugin ActiveDid // ================================================= /** * Update DailyNotification plugin when activeDid changes * * This method reconfigures the DailyNotification plugin with the new * activeDid and updates all related settings. */ 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); } } // ================================================= // NEW METHOD: ActiveDid Change Listener Management // ================================================= /** * Add listener for activeDid changes * * This method allows components to register listeners that will be * notified when the activeDid changes. */ addActiveDidChangeListener(listener: (newDid: string | null, oldDid: string | null) => void): void { this.activeDidChangeListeners.push(listener); } /** * Remove listener for activeDid changes */ removeActiveDidChangeListener(listener: (newDid: string | null, oldDid: string | null) => void): void { const index = this.activeDidChangeListeners.indexOf(listener); if (index > -1) { this.activeDidChangeListeners.splice(index, 1); } } /** * Notify all listeners of activeDid change */ private notifyActiveDidChange(newDid: string | null, oldDid: string | null): void { this.activeDidChangeListeners.forEach(listener => { try { listener(newDid, oldDid); } catch (error) { logger.error('[CapacitorPlatformService] Error in activeDid change listener:', error); } }); } // ================================================= // NEW METHOD: Initialize DailyNotification Plugin // ================================================= /** * Initialize DailyNotification plugin with TimeSafari configuration * * This method should be called after the database is initialized * to set up the DailyNotification plugin with TimeSafari-specific settings. */ 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; } } // ================================================= // NEW METHOD: Get Current ActiveDid // ================================================= /** * Get current activeDid from the database * * This method retrieves the current activeDid from the active_identity table * using the existing TimeSafari pattern. */ 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; } } // ================================================= // NEW METHOD: Enhanced loadNewStarredProjectChanges // ================================================= /** * Enhanced version of existing TimeSafari loadNewStarredProjectChanges method * * This method replaces the existing TimeSafari PWA method with plugin-enhanced * functionality while maintaining the same interface and behavior. */ 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 }; } } // ================================================= // NEW METHOD: Get TimeSafari Settings // ================================================= /** * Get TimeSafari settings using existing database patterns * * This method uses the existing TimeSafari database patterns to retrieve * settings that are needed for the DailyNotification plugin configuration. */ 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 {}; } } // ================================================= // NEW METHOD: Get TimeSafari Storage Adapter // ================================================= /** * Get TimeSafari storage adapter using existing patterns * * This method creates a storage adapter that uses the existing TimeSafari * database patterns for the DailyNotification plugin. */ 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; } }; } // ================================================= // NEW METHODS: Callback Handlers // ================================================= /** * 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 }); } // ================================================= // NEW METHOD: Get DailyNotification Status // ================================================= /** * 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 }; } // ================================================= // MODIFIED METHOD: Enhanced initializeDatabase // ================================================= 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; // NEW: 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; } } // ================================================= // EXISTING METHODS (unchanged - showing key ones) // ================================================= private async _initialize(): Promise { if (this.initialized) { return; } try { // Create/Open database this.db = await this.sqlite.createConnection( this.dbName, false, "no-encryption", 1, false, ); await this.db.open(); // Run migrations await this.runCapacitorMigrations(); this.initialized = true; logger.log( "[CapacitorPlatformService] SQLite database initialized successfully", ); // Start processing the queue after initialization this.processQueue(); } catch (error) { logger.error( "[CapacitorPlatformService] Error initializing SQLite database:", error, ); throw new Error( "[CapacitorPlatformService] Failed to initialize database", ); } } // ... (all other existing methods remain unchanged) /** * Gets the capabilities of the Capacitor platform * @returns Platform capabilities object */ getCapabilities(): PlatformCapabilities { const platform = Capacitor.getPlatform(); return { hasFileSystem: true, hasCamera: true, isMobile: true, // Capacitor is always mobile isIOS: platform === "ios", hasFileDownload: false, // Mobile platforms need sharing needsFileHandlingInstructions: true, // Mobile needs instructions isNativeApp: true, }; } /** * @see PlatformService.dbQuery */ async dbQuery(sql: string, params?: unknown[]): Promise { await this.waitForInitialization(); return this.queueOperation("query", sql, params || []); } /** * @see PlatformService.dbExec */ async dbExec( sql: string, params?: unknown[], ): Promise<{ changes: number; lastId?: number }> { await this.waitForInitialization(); return this.queueOperation<{ changes: number; lastId?: number }>( "run", sql, params || [], ); } // ... (all other existing methods remain unchanged) /** * Checks if running on Capacitor platform. * @returns true, as this is the Capacitor implementation */ isCapacitor(): boolean { return true; } /** * Checks if running on Electron platform. * @returns false, as this is Capacitor, not Electron */ isElectron(): boolean { return false; } /** * Checks if running on web platform. * @returns false, as this is not web */ isWeb(): boolean { return false; } // ... (all other existing methods remain unchanged) }