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
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:
-
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
-
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:
- Add the changes to existing TimeSafari PWA CapacitorPlatformService
- Test on Capacitor platforms (Android, iOS)
- Verify activeDid changes work correctly
- Gradually migrate individual methods to use plugin features
- 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.