docs: add TimeSafari PWA CapacitorPlatformService integration example

- Add comprehensive example showing DailyNotification plugin integration with existing TimeSafari PWA architecture
- Show how to extend CapacitorPlatformService and PlatformServiceMixin patterns
- Provide Vue.js component integration with existing TimeSafari patterns
- Include settings management, database operations, and platform detection
- Add migration strategy and testing approach for gradual adoption
- Show how to maintain existing interfaces while adding plugin features

This demonstrates how the plugin integrates with the actual TimeSafari PWA
CapacitorPlatformService and PlatformServiceMixin architecture patterns.
This commit is contained in:
Matthew Raymer
2025-10-08 08:10:20 +00:00
parent c8d545acd0
commit a3c92ec45e
2 changed files with 1084 additions and 0 deletions

View File

@@ -0,0 +1,594 @@
/**
* TimeSafari PWA - DailyNotification Plugin Integration Example
*
* This example shows how to integrate the DailyNotification plugin with the existing
* TimeSafari PWA architecture, specifically the CapacitorPlatformService and
* PlatformServiceMixin patterns.
*
* @author Matthew Raymer
* @version 1.0.0
*/
import { DailyNotification } from '@timesafari/daily-notification-plugin';
import { TimeSafariIntegrationService } from '@timesafari/daily-notification-plugin';
import { PlatformServiceFactory } from '@/services/PlatformServiceFactory';
import { Capacitor } from '@capacitor/core';
// TimeSafari PWA existing interfaces (from the actual project)
interface PlanSummaryAndPreviousClaim {
id: string;
title: string;
description: string;
lastUpdated: string;
previousClaim?: unknown;
}
interface StarredProjectsResponse {
data: Array<PlanSummaryAndPreviousClaim>;
hitLimit: boolean;
}
interface Settings {
accountDid?: string;
activeDid?: string;
apiServer?: string;
starredPlanHandleIds?: string[];
lastAckedStarredPlanChangesJwtId?: string;
[key: string]: unknown;
}
/**
* Enhanced CapacitorPlatformService with DailyNotification integration
*
* This extends the existing TimeSafari PWA CapacitorPlatformService to include
* DailyNotification plugin functionality while maintaining the same interface.
*/
export class EnhancedCapacitorPlatformService {
private platformService = PlatformServiceFactory.getInstance();
private dailyNotificationService: DailyNotification | null = null;
private integrationService: TimeSafariIntegrationService | null = null;
private initialized = false;
/**
* Initialize DailyNotification plugin with TimeSafari PWA configuration
*/
async initializeDailyNotification(): Promise<void> {
if (this.initialized) {
return;
}
try {
// Get current TimeSafari settings
const settings = await this.getTimeSafariSettings();
// 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 existing TimeSafari activeDid
activeDid: settings.activeDid || '',
// 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,
starredProjectsRetries: 3
}
},
// Network configuration using existing TimeSafari patterns
networkConfig: {
// Use existing TimeSafari HTTP client (if available)
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)
}
},
// Authentication configuration
authentication: {
jwt: {
secret: process.env.JWT_SECRET || 'timesafari-jwt-secret',
algorithm: 'HS256',
expirationMinutes: 60,
refreshThresholdMinutes: 10
}
},
// Observability configuration
logging: {
level: 'INFO',
enableRequestLogging: true,
enableResponseLogging: true,
enableErrorLogging: true,
redactSensitiveData: true
},
// Security configuration
security: {
certificatePinning: {
enabled: true,
pins: [
{
hostname: 'endorser.ch',
pins: ['sha256/YOUR_PIN_HERE']
}
]
}
}
});
// Initialize TimeSafari Integration Service
this.integrationService = TimeSafariIntegrationService.getInstance();
await this.integrationService.initialize({
activeDid: settings.activeDid || '',
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.initialized = true;
console.log('DailyNotification plugin initialized successfully with TimeSafari PWA');
} catch (error) {
console.error('Failed to initialize DailyNotification plugin:', error);
throw error;
}
}
/**
* Enhanced version of existing TimeSafari loadNewStarredProjectChanges method
*
* This replaces the existing method with plugin-enhanced functionality
* while maintaining the same interface and behavior.
*/
async loadNewStarredProjectChanges(): Promise<StarredProjectsResponse> {
if (!this.initialized) {
await this.initializeDailyNotification();
}
const settings = await this.getTimeSafariSettings();
if (!settings.activeDid || !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(
settings.activeDid,
settings.starredPlanHandleIds,
settings.lastAckedStarredPlanChangesJwtId
);
// Enhanced logging (optional)
console.log('Starred projects loaded successfully:', {
count: starredProjectChanges.data.length,
hitLimit: starredProjectChanges.hitLimit,
planIds: settings.starredPlanHandleIds.length
});
return starredProjectChanges;
} catch (error) {
// Same error handling as existing TimeSafari code
console.warn('[TimeSafari] Failed to load starred project changes:', error);
return { data: [], hitLimit: false };
}
}
/**
* Get TimeSafari settings using existing PlatformServiceMixin pattern
*/
private async getTimeSafariSettings(): Promise<Settings> {
try {
// Use existing TimeSafari settings retrieval pattern
const result = await this.platformService.dbQuery(
"SELECT * FROM settings WHERE id = 1"
);
if (!result?.values?.length) {
return {};
}
// Map database columns to values (existing TimeSafari pattern)
const settings: Settings = {};
result.columns.forEach((column, index) => {
if (column !== 'id') {
settings[column] = result.values[0][index];
}
});
// 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) {
console.error('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.platformService.dbExec(
"INSERT OR REPLACE INTO temp (id, data) VALUES (?, ?)",
[key, JSON.stringify(value)]
);
},
retrieve: async (key: string) => {
const result = await this.platformService.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 handlers that match existing TimeSafari error handling patterns
*/
private async handleStarredProjectsSuccess(data: StarredProjectsResponse): Promise<void> {
// Enhanced logging (optional)
console.log('Starred projects success callback:', {
count: data.data.length,
hitLimit: data.hitLimit
});
// Store results in TimeSafari temp table for UI access
await this.platformService.dbExec(
"INSERT OR REPLACE INTO temp (id, data) VALUES (?, ?)",
['starred_projects_latest', JSON.stringify(data)]
);
}
private async handleStarredProjectsError(error: Error): Promise<void> {
// Same error handling as existing TimeSafari code
console.warn('[TimeSafari] Failed to load starred project changes:', error);
// Store error in TimeSafari temp table for UI access
await this.platformService.dbExec(
"INSERT OR REPLACE INTO temp (id, data) VALUES (?, ?)",
['starred_projects_error', JSON.stringify({ error: error.message, timestamp: Date.now() })]
);
}
private async handleStarredProjectsComplete(result: unknown): Promise<void> {
// Handle completion
console.log('Starred projects fetch completed:', result);
}
/**
* Get plugin status for debugging
*/
async getDailyNotificationStatus(): Promise<{
initialized: boolean;
platform: string;
capabilities: unknown;
}> {
return {
initialized: this.initialized,
platform: Capacitor.getPlatform(),
capabilities: this.platformService.getCapabilities()
};
}
}
/**
* Vue.js Mixin for DailyNotification integration with TimeSafari PWA
*
* This mixin extends the existing PlatformServiceMixin to include
* DailyNotification functionality while maintaining the same patterns.
*/
export const TimeSafariDailyNotificationMixin = {
data() {
return {
// Existing TimeSafari data
activeDid: '',
starredPlanHandleIds: [] as string[],
lastAckedStarredPlanChangesJwtId: '',
numNewStarredProjectChanges: 0,
newStarredProjectChangesHitLimit: false,
// Plugin integration
dailyNotificationService: null as DailyNotification | null,
integrationService: null as TimeSafariIntegrationService | null,
enhancedPlatformService: null as EnhancedCapacitorPlatformService | null
};
},
computed: {
// Existing TimeSafari computed properties
isCapacitor(): boolean {
return this.platformService.isCapacitor();
},
isWeb(): boolean {
return this.platformService.isWeb();
},
isElectron(): boolean {
return this.platformService.isElectron();
}
},
async mounted() {
// Initialize DailyNotification when component mounts (only on Capacitor)
if (this.isCapacitor) {
await this.initializeDailyNotification();
}
},
methods: {
/**
* Initialize DailyNotification plugin with TimeSafari configuration
*/
async initializeDailyNotification(): Promise<void> {
try {
// Create enhanced platform service
this.enhancedPlatformService = new EnhancedCapacitorPlatformService();
// Initialize the plugin
await this.enhancedPlatformService.initializeDailyNotification();
console.log('DailyNotification initialized successfully in TimeSafari PWA');
} catch (error) {
console.error('Failed to initialize DailyNotification in TimeSafari PWA:', error);
}
},
/**
* Enhanced version of existing TimeSafari loadNewStarredProjectChanges method
*/
async loadNewStarredProjectChanges(): Promise<void> {
if (this.isCapacitor && this.enhancedPlatformService) {
// Use plugin-enhanced method on Capacitor
const result = await this.enhancedPlatformService.loadNewStarredProjectChanges();
this.numNewStarredProjectChanges = result.data.length;
this.newStarredProjectChangesHitLimit = result.hitLimit;
} else {
// Use existing web method in browser
await this.loadNewStarredProjectChangesWeb();
}
},
/**
* Existing web-only method (unchanged)
*/
async loadNewStarredProjectChangesWeb(): Promise<void> {
// Your existing web-only implementation
console.log('Using web-only method for starred projects');
},
/**
* Get DailyNotification status for debugging
*/
async getDailyNotificationStatus(): Promise<unknown> {
if (this.enhancedPlatformService) {
return await this.enhancedPlatformService.getDailyNotificationStatus();
}
return { initialized: false, platform: 'web' };
}
}
};
/**
* Usage example in a TimeSafari PWA Vue component
*/
export const TimeSafariHomeViewExample = {
name: 'TimeSafariHomeView',
mixins: [TimeSafariDailyNotificationMixin],
data() {
return {
// Existing TimeSafari data
activeDid: '',
starredPlanHandleIds: [] as string[],
lastAckedStarredPlanChangesJwtId: '',
numNewStarredProjectChanges: 0,
newStarredProjectChangesHitLimit: false
};
},
async mounted() {
// Load existing TimeSafari data
await this.loadTimeSafariData();
// Initialize DailyNotification (only on Capacitor)
if (this.isCapacitor) {
await this.initializeDailyNotification();
}
},
methods: {
/**
* Load existing TimeSafari data using PlatformServiceMixin
*/
async loadTimeSafariData(): Promise<void> {
try {
// Use existing TimeSafari settings pattern
const settings = await this.$settings();
this.activeDid = settings.activeDid || '';
this.starredPlanHandleIds = settings.starredPlanHandleIds || [];
this.lastAckedStarredPlanChangesJwtId = settings.lastAckedStarredPlanChangesJwtId || '';
} catch (error) {
console.error('Error loading TimeSafari data:', error);
}
},
/**
* Enhanced loadNewStarredProjectChanges method
*/
async loadNewStarredProjectChanges(): Promise<void> {
if (this.isCapacitor && this.enhancedPlatformService) {
// Use plugin-enhanced method on Capacitor
const result = await this.enhancedPlatformService.loadNewStarredProjectChanges();
this.numNewStarredProjectChanges = result.data.length;
this.newStarredProjectChangesHitLimit = result.hitLimit;
} else {
// Use existing web method in browser
await this.loadNewStarredProjectChangesWeb();
}
},
/**
* Existing web-only method (unchanged)
*/
async loadNewStarredProjectChangesWeb(): Promise<void> {
// Your existing web-only implementation
console.log('Using web-only method for starred projects');
}
}
};
/**
* Factory function to create enhanced platform service
*/
export function createEnhancedPlatformService(): EnhancedCapacitorPlatformService {
return new EnhancedCapacitorPlatformService();
}
/**
* Utility function to check if DailyNotification should be used
*/
export function shouldUseDailyNotification(): boolean {
return Capacitor.isNativePlatform();
}
/**
* Utility function to get platform-specific configuration
*/
export function getPlatformConfig(): {
usePlugin: boolean;
platform: string;
capabilities: unknown;
} {
const platform = Capacitor.getPlatform();
const platformService = PlatformServiceFactory.getInstance();
return {
usePlugin: Capacitor.isNativePlatform(),
platform,
capabilities: platformService.getCapabilities()
};
}