/** * TimeSafari PWA - CapacitorPlatformService Extension Example * * This example shows how to extend the existing CapacitorPlatformService * in the TimeSafari PWA to add DailyNotification plugin functionality. * * This represents the ACTUAL CHANGES needed to the existing TimeSafari PWA code. * * @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 functionality * * This shows the ACTUAL CHANGES needed to the existing TimeSafari PWA * CapacitorPlatformService class to add DailyNotification plugin support. */ 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; // ================================================= // EXISTING CONSTRUCTOR (unchanged) // ================================================= constructor() { this.sqlite = new SQLiteConnection(CapacitorSQLite); } // ================================================= // 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(); // 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: { 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.dailyNotificationInitialized = true; logger.log("[CapacitorPlatformService] DailyNotification plugin initialized successfully"); } catch (error) { logger.error("[CapacitorPlatformService] Failed to initialize DailyNotification plugin:", error); throw error; } } // ================================================= // 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. * * This is the ACTUAL METHOD that would replace the existing TimeSafari method. */ async loadNewStarredProjectChanges(): Promise { // Ensure DailyNotification is initialized if (!this.dailyNotificationInitialized) { 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) logger.log("[CapacitorPlatformService] 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 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 { // Use existing TimeSafari settings retrieval pattern const result = await this.dbQuery( "SELECT * FROM settings WHERE id = 1" ); 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]; } }); // 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 * * This method handles successful starred projects fetches and stores * the results in the existing TimeSafari database for UI access. */ private async handleStarredProjectsSuccess(data: StarredProjectsResponse): Promise { // Enhanced logging (optional) logger.log("[CapacitorPlatformService] Starred projects success callback:", { count: data.data.length, hitLimit: data.hitLimit }); // 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 * * This method handles starred projects fetch errors and stores * the error information in the existing TimeSafari database for UI access. */ 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() })] ); } /** * Callback handler for starred projects fetch completion * * This method handles the completion of starred projects fetches * and provides additional logging and cleanup. */ private async handleStarredProjectsComplete(result: unknown): Promise { // Handle completion logger.log("[CapacitorPlatformService] Starred projects fetch completed:", result); } // ================================================= // NEW METHOD: Get DailyNotification Status // ================================================= /** * Get DailyNotification plugin status for debugging * * This method provides status information about the DailyNotification * plugin for debugging and monitoring purposes. */ async getDailyNotificationStatus(): Promise<{ initialized: boolean; platform: string; capabilities: PlatformCapabilities; }> { return { initialized: this.dailyNotificationInitialized, platform: Capacitor.getPlatform(), capabilities: this.getCapabilities() }; } // ================================================= // EXISTING METHODS (unchanged - showing key ones) // ================================================= 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; } } 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(); // Set journal mode to WAL for better performance // await this.db.execute("PRAGMA journal_mode=WAL;"); // 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) }