diff --git a/docs/activedid-change-integration-guide.md b/docs/activedid-change-integration-guide.md new file mode 100644 index 0000000..b5ab2fe --- /dev/null +++ b/docs/activedid-change-integration-guide.md @@ -0,0 +1,422 @@ +# TimeSafari PWA - ActiveDid Change Integration Guide + +**Author**: Matthew Raymer +**Version**: 1.0.0 +**Created**: 2025-10-08 06:24:57 UTC + +## Overview + +This guide explains how the DailyNotification plugin integrates with the existing TimeSafari PWA activeDid change detection system. The TimeSafari PWA has a sophisticated activeDid change management system that the plugin must integrate with. + +## TimeSafari PWA ActiveDid Change System + +### How ActiveDid Changes Work + +#### 1. Database Storage +```sql +-- ActiveDid is stored in active_identity table (single source of truth) +UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1 +``` + +#### 2. PlatformServiceMixin Change Detection +```typescript +// In PlatformServiceMixin.ts +watch: { + currentActiveDid: { + handler(newDid: string | null, oldDid: string | null) { + if (newDid !== oldDid) { + logger.debug(`[PlatformServiceMixin] ActiveDid changed from ${oldDid} to ${newDid}`); + // Clear caches that might be affected by the change + } + }, + immediate: true, + }, +} +``` + +#### 3. Update Method +```typescript +// In PlatformServiceMixin.ts +async $updateActiveDid(newDid: string | null): Promise { + const oldDid = this._currentActiveDid; + this._currentActiveDid = newDid; + + if (newDid !== oldDid) { + // Write to active_identity table (single source of truth) + await this.$dbExec( + "UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1", + [newDid || ""] + ); + } +} +``` + +#### 4. Usage in Components +```typescript +// In IdentitySwitcherView.vue +async switchAccount(did?: string) { + // Update the active DID in the active_identity table + await this.$updateActiveDid(did); + + // Check if we need to load user-specific settings for the new DID + if (did) { + const newSettings = await this.$accountSettings(did); + // Update UI with new settings + } +} +``` + +## DailyNotification Plugin Integration + +### How the Plugin Handles ActiveDid Changes + +#### 1. Enhanced updateActiveDid Method +```typescript +// In CapacitorPlatformService.ts +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); + } +} +``` + +#### 2. Plugin Reconfiguration +```typescript +// In CapacitorPlatformService.ts +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: { + 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' + }); + } + + } catch (error) { + logger.error(`[CapacitorPlatformService] Failed to update DailyNotification plugin activeDid:`, error); + } +} +``` + +#### 3. Change Listener System +```typescript +// In CapacitorPlatformService.ts +private activeDidChangeListeners: Array<(newDid: string | null, oldDid: string | null) => void> = []; + +addActiveDidChangeListener(listener: (newDid: string | null, oldDid: string | null) => void): void { + this.activeDidChangeListeners.push(listener); +} + +removeActiveDidChangeListener(listener: (newDid: string | null, oldDid: string | null) => void): void { + const index = this.activeDidChangeListeners.indexOf(listener); + if (index > -1) { + this.activeDidChangeListeners.splice(index, 1); + } +} + +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); + } + }); +} +``` + +## Integration Flow + +### 1. User Switches Identity +``` +User clicks "Switch Account" → IdentitySwitcherView.switchAccount() → +$updateActiveDid() → CapacitorPlatformService.updateActiveDid() → +DailyNotification plugin reconfiguration +``` + +### 2. Plugin Reconfiguration Process +``` +1. Update database (existing TimeSafari pattern) +2. Update local tracking +3. Notify change listeners +4. Reconfigure DailyNotification plugin +5. Update TimeSafari Integration Service +6. Log the change +``` + +### 3. Settings Update Process +``` +1. Get new settings for new activeDid +2. Update plugin configuration with new settings +3. Update integration service with new activeDid +4. Clear any cached data for old activeDid +5. Initialize new activeDid-specific data +``` + +## Required Changes to Existing Code + +### File: `src/services/platforms/CapacitorPlatformService.ts` + +#### 1. Add New 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; + private activeDidChangeListeners: Array<(newDid: string | null, oldDid: string | null) => void> = []; +} +``` + +#### 2. 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; + + // 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}`); +} +``` + +#### 3. Add New Methods +```typescript +// Add these new methods to the class +private async updateDailyNotificationActiveDid(newDid: string, oldDid: string | null): Promise { /* ... */ } +private async getCurrentActiveDid(): Promise { /* ... */ } +addActiveDidChangeListener(listener: (newDid: string | null, oldDid: string | null) => void): void { /* ... */ } +removeActiveDidChangeListener(listener: (newDid: string | null, oldDid: string | null) => void): void { /* ... */ } +private notifyActiveDidChange(newDid: string | null, oldDid: string | null): void { /* ... */ } +``` + +## PlatformServiceMixin Integration + +### File: `src/utils/PlatformServiceMixin.ts` + +#### 1. Add DailyNotification Methods +```typescript +methods: { + // ... existing methods ... + + /** + * Initialize DailyNotification plugin (Capacitor only) + */ + async $initializeDailyNotification(): Promise { + if (this.isCapacitor) { + await this.platformService.initializeDailyNotification(); + } + }, + + /** + * Enhanced loadNewStarredProjectChanges method + */ + async $loadNewStarredProjectChanges(): Promise { + if (this.isCapacitor) { + return await this.platformService.loadNewStarredProjectChanges(); + } else { + // Fall back to existing web method + return await this.$loadNewStarredProjectChangesWeb(); + } + } +} +``` + +## Vue Component Integration + +### File: `src/views/HomeView.vue` (or similar) + +#### 1. Add DailyNotification Initialization +```typescript +export default defineComponent({ + name: 'HomeView', + + mixins: [PlatformServiceMixin], + + async mounted() { + // Initialize DailyNotification (only on Capacitor) + if (this.isCapacitor) { + await this.$initializeDailyNotification(); + } + }, + + methods: { + /** + * Enhanced loadNewStarredProjectChanges method + */ + async loadNewStarredProjectChanges(): Promise { + if (this.isCapacitor) { + // Use plugin-enhanced method on Capacitor + const result = await this.$loadNewStarredProjectChanges(); + this.numNewStarredProjectChanges = result.data.length; + this.newStarredProjectChangesHitLimit = result.hitLimit; + } else { + // Use existing web method in browser + await this.loadNewStarredProjectChangesWeb(); + } + } + } +}); +``` + +## Key Benefits + +### 1. Seamless ActiveDid Change Handling +- **Automatic Reconfiguration**: Plugin automatically reconfigures when activeDid changes +- **Settings Synchronization**: New activeDid settings are automatically loaded and applied +- **Error Handling**: Robust error handling for activeDid change failures +- **Logging**: Comprehensive logging for debugging activeDid changes + +### 2. Integration with Existing Patterns +- **Uses Existing Methods**: Leverages existing `updateActiveDid` method +- **Database Consistency**: Uses existing `active_identity` table pattern +- **Change Detection**: Integrates with existing change detection system +- **Settings Management**: Uses existing settings retrieval patterns + +### 3. Enhanced Functionality +- **Background Fetching**: Continues to work with new activeDid +- **Notification Scheduling**: Updates notifications for new activeDid +- **Storage Management**: Manages storage for multiple activeDids +- **Observability**: Tracks activeDid changes in logs and metrics + +## Testing Strategy + +### 1. ActiveDid Change Testing +```typescript +// Test activeDid change handling +const testActiveDidChange = async () => { + // Switch to new activeDid + await platformService.updateActiveDid('new-did-123'); + + // Verify plugin is reconfigured + const status = await platformService.getDailyNotificationStatus(); + assert.equal(status.currentActiveDid, 'new-did-123'); + + // Test starred projects fetch with new activeDid + const result = await platformService.loadNewStarredProjectChanges(); + assert.ok(result.data !== undefined); +}; +``` + +### 2. Settings Synchronization Testing +```typescript +// Test settings synchronization +const testSettingsSynchronization = async () => { + // Switch activeDid + await platformService.updateActiveDid('new-did-123'); + + // Verify settings are loaded for new activeDid + const settings = await platformService.getTimeSafariSettings(); + assert.equal(settings.activeDid, 'new-did-123'); + + // Verify plugin configuration is updated + const config = await DailyNotification.getConfiguration(); + assert.equal(config.timesafariConfig.activeDid, 'new-did-123'); +}; +``` + +### 3. Error Handling Testing +```typescript +// Test error handling during activeDid changes +const testErrorHandling = async () => { + try { + // Switch to invalid activeDid + await platformService.updateActiveDid('invalid-did'); + + // Verify error is handled gracefully + const status = await platformService.getDailyNotificationStatus(); + assert.ok(status.initialized); // Plugin should remain initialized + + } catch (error) { + // Verify error is logged + assert.ok(error.message.includes('activeDid')); + } +}; +``` + +## Common Issues and Solutions + +### Issue 1: Plugin Not Updating on ActiveDid Change +**Solution**: Ensure `updateDailyNotificationActiveDid` is called in the `updateActiveDid` method + +### Issue 2: Settings Not Loading for New ActiveDid +**Solution**: Verify `getTimeSafariSettings` uses the current activeDid from database + +### Issue 3: Cached Data from Old ActiveDid +**Solution**: Clear plugin caches when activeDid changes + +### Issue 4: Background Fetching with Wrong ActiveDid +**Solution**: Ensure plugin reconfiguration includes new activeDid in all requests + +## Conclusion + +The DailyNotification plugin integrates seamlessly with the existing TimeSafari PWA activeDid change system by: + +- **Extending the existing `updateActiveDid` method** to handle plugin reconfiguration +- **Using the existing change detection patterns** from PlatformServiceMixin +- **Maintaining database consistency** with the existing `active_identity` table +- **Providing robust error handling** for activeDid change failures +- **Supporting multiple activeDids** with proper isolation and cleanup + +The integration ensures that the plugin automatically adapts to activeDid changes while maintaining the same interface and behavior as the existing TimeSafari PWA code. + +--- + +**Key Takeaway**: The CapacitorPlatformService **does know** when activeDid changes through the existing `updateActiveDid` method, and the DailyNotification plugin integrates with this system to automatically reconfigure when the user switches identities. diff --git a/examples/capacitor-platform-service-activedid-integration.ts b/examples/capacitor-platform-service-activedid-integration.ts new file mode 100644 index 0000000..8626c71 --- /dev/null +++ b/examples/capacitor-platform-service-activedid-integration.ts @@ -0,0 +1,804 @@ +/** + * 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) +}