From bf511055c16aa97d3f85cdc40569145142586646 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Wed, 8 Oct 2025 08:58:32 +0000 Subject: [PATCH] docs: add clean CapacitorPlatformService integration without isCapacitor flags - Add example showing DailyNotification plugin integration ONLY in Capacitor classes - Remove all isCapacitor flags since plugin is only used on Capacitor platforms - Show actual changes needed to existing TimeSafari PWA CapacitorPlatformService - Include activeDid change handling and plugin reconfiguration - Provide clean summary of exact code changes needed - Focus on Capacitor-specific implementation without platform detection This gives a cleaner integration approach where plugin code only touches Capacitor classes and doesn't need platform detection flags. --- ...apacitor-platform-service-clean-changes.md | 577 ++++++++++++++ ...itor-platform-service-clean-integration.ts | 746 ++++++++++++++++++ 2 files changed, 1323 insertions(+) create mode 100644 docs/capacitor-platform-service-clean-changes.md create mode 100644 examples/capacitor-platform-service-clean-integration.ts diff --git a/docs/capacitor-platform-service-clean-changes.md b/docs/capacitor-platform-service-clean-changes.md new file mode 100644 index 0000000..765da64 --- /dev/null +++ b/docs/capacitor-platform-service-clean-changes.md @@ -0,0 +1,577 @@ +# 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.** diff --git a/examples/capacitor-platform-service-clean-integration.ts b/examples/capacitor-platform-service-clean-integration.ts new file mode 100644 index 0000000..f698d5e --- /dev/null +++ b/examples/capacitor-platform-service-clean-integration.ts @@ -0,0 +1,746 @@ +/** + * TimeSafari PWA - CapacitorPlatformService Clean Integration Example + * + * This example shows the ACTUAL CHANGES needed to the existing TimeSafari PWA + * CapacitorPlatformService to add DailyNotification plugin functionality. + * + * The plugin code ONLY touches Capacitor classes - no isCapacitor flags needed. + * + * @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 Integration + * + * This shows the ACTUAL CHANGES needed to the existing TimeSafari PWA + * CapacitorPlatformService class. The plugin code ONLY touches this class. + */ +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; + + // ================================================= + // 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; + + // 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 + */ + 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: Initialize DailyNotification Plugin + // ================================================= + + /** + * 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; + } + } + + // ================================================= + // NEW METHOD: Get Current ActiveDid + // ================================================= + + /** + * 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; + } + } + + // ================================================= + // 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 + */ + 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 + */ + 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) +}