Files
daily-notification-plugin/docs/capacitor-platform-service-clean-changes.md
Matthew Raymer bf511055c1 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.
2025-10-08 08:58:32 +00:00

578 lines
18 KiB
Markdown

# 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<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)
```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<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
```typescript
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)
```typescript
/**
* 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
```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.**