You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

18 KiB

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)

// ADD THESE IMPORTS
import { DailyNotification } from '@timesafari/daily-notification-plugin';
import { TimeSafariIntegrationService } from '@timesafari/daily-notification-plugin';

2. Add New Interfaces (after existing interfaces)

// ADD THESE INTERFACES
interface PlanSummaryAndPreviousClaim {
  id: string;
  title: string;
  description: string;
  lastUpdated: string;
  previousClaim?: unknown;
}

interface StarredProjectsResponse {
  data: Array<PlanSummaryAndPreviousClaim>;
  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)

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

async updateActiveDid(did: string): Promise<void> {
  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

private async initializeDatabase(): Promise<void> {
  // 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)

  /**
   * Initialize DailyNotification plugin with TimeSafari configuration
   */
  async initializeDailyNotification(): Promise<void> {
    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<StarredProjectsResponse> {
    // 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<void> {
    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<string | null> {
    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<TimeSafariSettings> {
    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<void> {
    // 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<void> {
    // 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<void> {
    // 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

{
  "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.