Browse Source
- Remove older capacitor integration examples and guides - Remove duplicate timesafari integration examples - Remove redundant daily notification setup guides - Remove overlapping architecture diagrams and decision guides - Keep only the clean, final versions: - capacitor-platform-service-clean-integration.ts - capacitor-platform-service-clean-changes.md - daily-notification-timesafari-setup.ts - timesafari-integration-example.ts - host-request-configuration.md This reduces file proliferation and keeps only the essential, non-overlapping documentation and examples.master
14 changed files with 0 additions and 6813 deletions
@ -1,422 +0,0 @@ |
|||
# 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<void> { |
|||
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<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; |
|||
|
|||
// 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<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: { |
|||
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<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; |
|||
|
|||
// 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<void> { /* ... */ } |
|||
private async getCurrentActiveDid(): Promise<string | null> { /* ... */ } |
|||
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<void> { |
|||
if (this.isCapacitor) { |
|||
await this.platformService.initializeDailyNotification(); |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* Enhanced loadNewStarredProjectChanges method |
|||
*/ |
|||
async $loadNewStarredProjectChanges(): Promise<StarredProjectsResponse> { |
|||
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<void> { |
|||
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. |
@ -1,492 +0,0 @@ |
|||
# DailyNotification Plugin - Capacitor Integration Guide |
|||
|
|||
**Author**: Matthew Raymer |
|||
**Version**: 1.0.0 |
|||
**Created**: 2025-10-08 06:24:57 UTC |
|||
|
|||
## Overview |
|||
|
|||
The DailyNotification plugin is **specifically designed for Capacitor-based mobile applications**. It provides native mobile functionality that is not available in web-only PWAs. This guide explains when and how to use the plugin in your TimeSafari PWA. |
|||
|
|||
## When to Use the Plugin |
|||
|
|||
### ✅ Use the Plugin When: |
|||
- **Capacitor is installed** in your TimeSafari PWA project |
|||
- **Building for mobile platforms** (Android, iOS) |
|||
- **Need native mobile features** like: |
|||
- Background notifications |
|||
- Native notification scheduling |
|||
- Background task execution |
|||
- Platform-specific notification channels |
|||
- Battery optimization handling |
|||
- Exact alarm permissions |
|||
|
|||
### ❌ Don't Use the Plugin When: |
|||
- **Web-only PWA** (no Capacitor) |
|||
- **Desktop-only Electron app** (though Electron support is available) |
|||
- **Server-side rendering** (SSR) |
|||
- **Static site generation** (SSG) |
|||
|
|||
## Integration Scenarios |
|||
|
|||
### Scenario 1: Web-Only PWA (No Plugin Needed) |
|||
|
|||
```typescript |
|||
// In your web-only TimeSafari PWA |
|||
// You continue using your existing code as-is |
|||
|
|||
private async loadNewStarredProjectChanges() { |
|||
if (this.activeDid && this.starredPlanHandleIds.length > 0) { |
|||
try { |
|||
const starredProjectChanges = await getStarredProjectsWithChanges( |
|||
this.axios, |
|||
this.apiServer, |
|||
this.activeDid, |
|||
this.starredPlanHandleIds, |
|||
this.lastAckedStarredPlanChangesJwtId, |
|||
); |
|||
this.numNewStarredProjectChanges = starredProjectChanges.data.length; |
|||
this.newStarredProjectChangesHitLimit = starredProjectChanges.hitLimit; |
|||
} catch (error) { |
|||
console.warn("[HomeView] Failed to load starred project changes:", error); |
|||
this.numNewStarredProjectChanges = 0; |
|||
this.newStarredProjectChangesHitLimit = false; |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
**What happens:** |
|||
- Your existing code works perfectly in the browser |
|||
- No plugin integration needed |
|||
- No native mobile features (background notifications, etc.) |
|||
- Limited to web browser capabilities |
|||
|
|||
### Scenario 2: Capacitor-Based PWA (Plugin Recommended) |
|||
|
|||
```typescript |
|||
// In your Capacitor-based TimeSafari PWA |
|||
import { DailyNotification } from '@timesafari/daily-notification-plugin'; |
|||
import { Capacitor } from '@capacitor/core'; |
|||
|
|||
class TimeSafariHomeView { |
|||
async initialize() { |
|||
// Check if running in Capacitor |
|||
if (Capacitor.isNativePlatform()) { |
|||
// Initialize plugin for native mobile features |
|||
await this.setupDailyNotification(); |
|||
} else { |
|||
// Use existing web-only code |
|||
console.log('Running in web browser - using existing code'); |
|||
} |
|||
} |
|||
|
|||
async setupDailyNotification() { |
|||
// Only runs on native platforms (Android, iOS) |
|||
await DailyNotification.configure({ |
|||
timesafariConfig: { |
|||
activeDid: this.activeDid, |
|||
endpoints: { |
|||
projectsLastUpdated: `${this.apiServer}/api/v2/report/plansLastUpdatedBetween` |
|||
}, |
|||
starredProjectsConfig: { |
|||
enabled: true, |
|||
starredPlanHandleIds: this.starredPlanHandleIds, |
|||
lastAckedJwtId: this.lastAckedStarredPlanChangesJwtId, |
|||
fetchInterval: '0 8 * * *' |
|||
} |
|||
}, |
|||
networkConfig: { |
|||
httpClient: this.axios, |
|||
baseURL: this.apiServer, |
|||
timeout: 30000 |
|||
} |
|||
}); |
|||
} |
|||
|
|||
async loadNewStarredProjectChanges() { |
|||
if (Capacitor.isNativePlatform()) { |
|||
// Use plugin-enhanced method on native platforms |
|||
await this.loadNewStarredProjectChangesNative(); |
|||
} else { |
|||
// Use existing web method in browser |
|||
await this.loadNewStarredProjectChangesWeb(); |
|||
} |
|||
} |
|||
|
|||
async loadNewStarredProjectChangesNative() { |
|||
// Plugin-enhanced method with native features |
|||
const starredProjectChanges = await this.integrationService.getStarredProjectsWithChanges( |
|||
this.activeDid, |
|||
this.starredPlanHandleIds, |
|||
this.lastAckedStarredPlanChangesJwtId |
|||
); |
|||
this.numNewStarredProjectChanges = starredProjectChanges.data.length; |
|||
this.newStarredProjectChangesHitLimit = starredProjectChanges.hitLimit; |
|||
} |
|||
|
|||
async loadNewStarredProjectChangesWeb() { |
|||
// Existing web-only method |
|||
const starredProjectChanges = await getStarredProjectsWithChanges( |
|||
this.axios, |
|||
this.apiServer, |
|||
this.activeDid, |
|||
this.starredPlanHandleIds, |
|||
this.lastAckedStarredPlanChangesJwtId, |
|||
); |
|||
this.numNewStarredProjectChanges = starredProjectChanges.data.length; |
|||
this.newStarredProjectChangesHitLimit = starredProjectChanges.hitLimit; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## Platform Detection |
|||
|
|||
### Check if Capacitor is Available |
|||
|
|||
```typescript |
|||
import { Capacitor } from '@capacitor/core'; |
|||
|
|||
// Check if running in Capacitor |
|||
if (Capacitor.isNativePlatform()) { |
|||
// Use plugin for native mobile features |
|||
await this.setupDailyNotification(); |
|||
} else { |
|||
// Use existing web-only code |
|||
console.log('Running in web browser'); |
|||
} |
|||
|
|||
// Check specific platform |
|||
if (Capacitor.getPlatform() === 'android') { |
|||
// Android-specific configuration |
|||
} else if (Capacitor.getPlatform() === 'ios') { |
|||
// iOS-specific configuration |
|||
} else { |
|||
// Web browser |
|||
} |
|||
``` |
|||
|
|||
### Conditional Plugin Loading |
|||
|
|||
```typescript |
|||
// Only load plugin when needed |
|||
let DailyNotification: any = null; |
|||
let TimeSafariIntegrationService: any = null; |
|||
|
|||
if (Capacitor.isNativePlatform()) { |
|||
// Dynamically import plugin only on native platforms |
|||
const plugin = await import('@timesafari/daily-notification-plugin'); |
|||
DailyNotification = plugin.DailyNotification; |
|||
TimeSafariIntegrationService = plugin.TimeSafariIntegrationService; |
|||
} |
|||
|
|||
// Use plugin conditionally |
|||
if (DailyNotification) { |
|||
await DailyNotification.configure({...}); |
|||
} else { |
|||
// Use existing web-only code |
|||
} |
|||
``` |
|||
|
|||
## Capacitor Configuration |
|||
|
|||
### 1. Install Capacitor (if not already installed) |
|||
|
|||
```bash |
|||
# Install Capacitor |
|||
npm install @capacitor/core @capacitor/cli |
|||
|
|||
# Initialize Capacitor |
|||
npx cap init |
|||
|
|||
# Add platforms |
|||
npx cap add android |
|||
npx cap add ios |
|||
``` |
|||
|
|||
### 2. Configure Capacitor for TimeSafari PWA |
|||
|
|||
```typescript |
|||
// capacitor.config.ts |
|||
import { CapacitorConfig } from '@capacitor/cli'; |
|||
|
|||
const config: CapacitorConfig = { |
|||
appId: 'app.timesafari', |
|||
appName: 'TimeSafari', |
|||
webDir: 'dist', |
|||
plugins: { |
|||
DailyNotification: { |
|||
// Plugin configuration |
|||
storage: 'tiered', |
|||
ttlSeconds: 1800, |
|||
enableETagSupport: true, |
|||
enableErrorHandling: true, |
|||
enablePerformanceOptimization: true |
|||
} |
|||
} |
|||
}; |
|||
|
|||
export default config; |
|||
``` |
|||
|
|||
### 3. Build and Sync |
|||
|
|||
```bash |
|||
# Build your PWA |
|||
npm run build |
|||
|
|||
# Sync with Capacitor |
|||
npx cap sync |
|||
|
|||
# Open in IDE |
|||
npx cap open android |
|||
npx cap open ios |
|||
``` |
|||
|
|||
## Platform-Specific Features |
|||
|
|||
### Android Features (Only with Plugin) |
|||
|
|||
```typescript |
|||
// Android-specific features |
|||
if (Capacitor.getPlatform() === 'android') { |
|||
// WorkManager for background tasks |
|||
// AlarmManager for exact notifications |
|||
// Notification channels |
|||
// Battery optimization handling |
|||
// Exact alarm permissions |
|||
} |
|||
``` |
|||
|
|||
### iOS Features (Only with Plugin) |
|||
|
|||
```typescript |
|||
// iOS-specific features |
|||
if (Capacitor.getPlatform() === 'ios') { |
|||
// BGTaskScheduler for background tasks |
|||
// UNUserNotificationCenter for notifications |
|||
// Background App Refresh |
|||
// Provisional authorization |
|||
// Notification categories |
|||
} |
|||
``` |
|||
|
|||
### Web Features (Existing Code) |
|||
|
|||
```typescript |
|||
// Web browser features |
|||
if (!Capacitor.isNativePlatform()) { |
|||
// Service Worker (if implemented) |
|||
// Web Notifications API |
|||
// Local Storage |
|||
// IndexedDB |
|||
// Your existing web-only code |
|||
} |
|||
``` |
|||
|
|||
## Integration Strategy |
|||
|
|||
### 1. Progressive Enhancement |
|||
|
|||
```typescript |
|||
class TimeSafariHomeView { |
|||
async initialize() { |
|||
// Always initialize with existing web code |
|||
await this.initializeWeb(); |
|||
|
|||
// Enhance with plugin if on native platform |
|||
if (Capacitor.isNativePlatform()) { |
|||
await this.initializeNative(); |
|||
} |
|||
} |
|||
|
|||
async initializeWeb() { |
|||
// Your existing web-only initialization |
|||
console.log('Initialized web version'); |
|||
} |
|||
|
|||
async initializeNative() { |
|||
// Plugin-enhanced initialization |
|||
await this.setupDailyNotification(); |
|||
console.log('Initialized native version with plugin'); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### 2. Feature Detection |
|||
|
|||
```typescript |
|||
class TimeSafariHomeView { |
|||
private isNative = Capacitor.isNativePlatform(); |
|||
private hasPlugin = false; |
|||
|
|||
async initialize() { |
|||
if (this.isNative) { |
|||
try { |
|||
await this.setupDailyNotification(); |
|||
this.hasPlugin = true; |
|||
console.log('Plugin initialized successfully'); |
|||
} catch (error) { |
|||
console.warn('Plugin initialization failed, falling back to web mode'); |
|||
this.hasPlugin = false; |
|||
} |
|||
} |
|||
} |
|||
|
|||
async loadNewStarredProjectChanges() { |
|||
if (this.hasPlugin) { |
|||
// Use plugin-enhanced method |
|||
await this.loadNewStarredProjectChangesNative(); |
|||
} else { |
|||
// Use existing web method |
|||
await this.loadNewStarredProjectChangesWeb(); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### 3. Graceful Degradation |
|||
|
|||
```typescript |
|||
class TimeSafariHomeView { |
|||
async loadNewStarredProjectChanges() { |
|||
try { |
|||
if (this.hasPlugin) { |
|||
// Try plugin-enhanced method first |
|||
await this.loadNewStarredProjectChangesNative(); |
|||
} else { |
|||
// Fall back to web method |
|||
await this.loadNewStarredProjectChangesWeb(); |
|||
} |
|||
} catch (error) { |
|||
// If plugin fails, fall back to web method |
|||
console.warn('Plugin method failed, falling back to web method'); |
|||
await this.loadNewStarredProjectChangesWeb(); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## Build Configuration |
|||
|
|||
### 1. Package.json Scripts |
|||
|
|||
```json |
|||
{ |
|||
"scripts": { |
|||
"build": "vite build", |
|||
"build:web": "vite build", |
|||
"build:android": "vite build && npx cap sync android", |
|||
"build:ios": "vite build && npx cap sync ios", |
|||
"dev:web": "vite", |
|||
"dev:android": "vite && npx cap run android", |
|||
"dev:ios": "vite && npx cap run ios" |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### 2. Environment Variables |
|||
|
|||
```bash |
|||
# .env |
|||
VITE_CAPACITOR_ENABLED=true |
|||
VITE_PLUGIN_ENABLED=true |
|||
|
|||
# .env.web (web-only build) |
|||
VITE_CAPACITOR_ENABLED=false |
|||
VITE_PLUGIN_ENABLED=false |
|||
``` |
|||
|
|||
### 3. Conditional Imports |
|||
|
|||
```typescript |
|||
// Only import plugin when needed |
|||
const loadPlugin = async () => { |
|||
if (import.meta.env.VITE_PLUGIN_ENABLED === 'true') { |
|||
return await import('@timesafari/daily-notification-plugin'); |
|||
} |
|||
return null; |
|||
}; |
|||
|
|||
const plugin = await loadPlugin(); |
|||
if (plugin) { |
|||
await plugin.DailyNotification.configure({...}); |
|||
} |
|||
``` |
|||
|
|||
## Testing Strategy |
|||
|
|||
### 1. Web Testing |
|||
|
|||
```bash |
|||
# Test web-only version |
|||
npm run dev:web |
|||
# Test in browser - should use existing code |
|||
``` |
|||
|
|||
### 2. Native Testing |
|||
|
|||
```bash |
|||
# Test Android version |
|||
npm run dev:android |
|||
# Test on Android device/emulator - should use plugin |
|||
|
|||
# Test iOS version |
|||
npm run dev:ios |
|||
# Test on iOS device/simulator - should use plugin |
|||
``` |
|||
|
|||
### 3. Cross-Platform Testing |
|||
|
|||
```typescript |
|||
// Test both implementations |
|||
const testBothImplementations = async () => { |
|||
// Test web implementation |
|||
const webResult = await this.loadNewStarredProjectChangesWeb(); |
|||
|
|||
// Test native implementation (if available) |
|||
if (this.hasPlugin) { |
|||
const nativeResult = await this.loadNewStarredProjectChangesNative(); |
|||
|
|||
// Compare results |
|||
console.log('Web result:', webResult); |
|||
console.log('Native result:', nativeResult); |
|||
} |
|||
}; |
|||
``` |
|||
|
|||
## Common Questions |
|||
|
|||
### Q: Do I need to install the plugin for web-only PWAs? |
|||
**A:** No, the plugin is only needed for Capacitor-based mobile apps. Web-only PWAs continue using your existing code. |
|||
|
|||
### Q: What happens if I install the plugin but don't use Capacitor? |
|||
**A:** The plugin will not work in web browsers. You should use platform detection to only initialize the plugin on native platforms. |
|||
|
|||
### Q: Can I use the plugin in Electron? |
|||
**A:** Yes, the plugin supports Electron, but you need to configure it specifically for Electron. |
|||
|
|||
### Q: How do I handle different platforms? |
|||
**A:** Use `Capacitor.getPlatform()` to detect the platform and configure the plugin accordingly. |
|||
|
|||
### Q: What if the plugin fails to initialize? |
|||
**A:** Implement graceful degradation to fall back to your existing web-only code. |
|||
|
|||
## Conclusion |
|||
|
|||
The DailyNotification plugin is **only needed when using Capacitor** for mobile app development. For web-only PWAs, you continue using your existing TimeSafari code. The plugin provides native mobile features like background notifications, exact alarm scheduling, and platform-specific optimizations that are not available in web browsers. |
|||
|
|||
**Key Points:** |
|||
- ✅ **Use plugin with Capacitor** for mobile apps (Android, iOS) |
|||
- ❌ **Don't use plugin** for web-only PWAs |
|||
- 🔄 **Use platform detection** to conditionally load the plugin |
|||
- 🛡️ **Implement graceful degradation** to fall back to web code |
|||
- 🧪 **Test both implementations** to ensure compatibility |
|||
|
|||
--- |
|||
|
|||
**Next Steps:** |
|||
1. Check if your TimeSafari PWA uses Capacitor |
|||
2. If yes, integrate the plugin with platform detection |
|||
3. If no, continue using your existing web-only code |
|||
4. Test both web and native implementations |
@ -1,427 +0,0 @@ |
|||
# TimeSafari PWA - CapacitorPlatformService Changes Summary |
|||
|
|||
**Author**: Matthew Raymer |
|||
**Version**: 1.0.0 |
|||
**Created**: 2025-10-08 06:24:57 UTC |
|||
|
|||
## Overview |
|||
|
|||
This document summarizes the **exact changes** needed to the existing TimeSafari PWA `CapacitorPlatformService` to add DailyNotification plugin functionality. These are the actual modifications required to the existing codebase. |
|||
|
|||
## 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; |
|||
} |
|||
``` |
|||
|
|||
#### 4. 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(); |
|||
|
|||
// Configure DailyNotification plugin with TimeSafari data |
|||
await DailyNotification.configure({ |
|||
timesafariConfig: { |
|||
activeDid: settings.activeDid || '', |
|||
endpoints: { |
|||
projectsLastUpdated: `${settings.apiServer}/api/v2/report/plansLastUpdatedBetween` |
|||
}, |
|||
starredProjectsConfig: { |
|||
enabled: true, |
|||
starredPlanHandleIds: settings.starredPlanHandleIds || [], |
|||
lastAckedJwtId: settings.lastAckedStarredPlanChangesJwtId || '', |
|||
fetchInterval: '0 8 * * *' |
|||
} |
|||
}, |
|||
networkConfig: { |
|||
baseURL: settings.apiServer || 'https://endorser.ch', |
|||
timeout: 30000 |
|||
} |
|||
}); |
|||
|
|||
// Initialize TimeSafari Integration Service |
|||
this.integrationService = TimeSafariIntegrationService.getInstance(); |
|||
await this.integrationService.initialize({ |
|||
activeDid: settings.activeDid || '', |
|||
storageAdapter: this.getTimeSafariStorageAdapter(), |
|||
endorserApiBaseUrl: settings.apiServer || 'https://endorser.ch' |
|||
}); |
|||
|
|||
this.dailyNotificationInitialized = true; |
|||
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(); |
|||
|
|||
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 |
|||
); |
|||
|
|||
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 }; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Get TimeSafari settings using existing database patterns |
|||
*/ |
|||
private async getTimeSafariSettings(): Promise<TimeSafariSettings> { |
|||
try { |
|||
const result = await this.dbQuery("SELECT * FROM settings WHERE id = 1"); |
|||
|
|||
if (!result?.values?.length) { |
|||
return {}; |
|||
} |
|||
|
|||
const settings: TimeSafariSettings = {}; |
|||
result.columns.forEach((column, index) => { |
|||
if (column !== 'id') { |
|||
settings[column] = result.values[0][index]; |
|||
} |
|||
}); |
|||
|
|||
// Handle JSON field parsing |
|||
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 { |
|||
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 handlers for DailyNotification plugin |
|||
*/ |
|||
private async handleStarredProjectsSuccess(data: StarredProjectsResponse): Promise<void> { |
|||
logger.log("[CapacitorPlatformService] Starred projects success callback:", { |
|||
count: data.data.length, |
|||
hitLimit: data.hitLimit |
|||
}); |
|||
|
|||
// 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)] |
|||
); |
|||
} |
|||
|
|||
private async handleStarredProjectsError(error: Error): Promise<void> { |
|||
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() })] |
|||
); |
|||
} |
|||
|
|||
private async handleStarredProjectsComplete(result: unknown): Promise<void> { |
|||
logger.log("[CapacitorPlatformService] Starred projects fetch completed:", result); |
|||
} |
|||
|
|||
/** |
|||
* Get DailyNotification plugin status for debugging |
|||
*/ |
|||
async getDailyNotificationStatus(): Promise<{ |
|||
initialized: boolean; |
|||
platform: string; |
|||
capabilities: PlatformCapabilities; |
|||
}> { |
|||
return { |
|||
initialized: this.dailyNotificationInitialized, |
|||
platform: Capacitor.getPlatform(), |
|||
capabilities: this.getCapabilities() |
|||
}; |
|||
} |
|||
``` |
|||
|
|||
#### 5. Modify Existing Method (update the `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; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## Required Changes to PlatformServiceMixin |
|||
|
|||
### File: `src/utils/PlatformServiceMixin.ts` |
|||
|
|||
#### 1. Add New Methods (in the methods section) |
|||
|
|||
```typescript |
|||
methods: { |
|||
// ... existing methods ... |
|||
|
|||
/** |
|||
* Initialize DailyNotification plugin (Capacitor only) |
|||
*/ |
|||
async $initializeDailyNotification(): Promise<void> { |
|||
if (this.isCapacitor) { |
|||
await this.platformService.initializeDailyNotification(); |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* Enhanced loadNewStarredProjectChanges method |
|||
*/ |
|||
async $loadNewStarredProjectChanges(): Promise<StarredProjectsResponse> { |
|||
if (this.isCapacitor) { |
|||
return await this.platformService.loadNewStarredProjectChanges(); |
|||
} else { |
|||
// Fall back to existing web method |
|||
return await this.$loadNewStarredProjectChangesWeb(); |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* Get DailyNotification status for debugging |
|||
*/ |
|||
async $getDailyNotificationStatus(): Promise<unknown> { |
|||
if (this.isCapacitor) { |
|||
return await this.platformService.getDailyNotificationStatus(); |
|||
} |
|||
return { initialized: false, platform: 'web' }; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## Required Changes to Vue Components |
|||
|
|||
### File: `src/views/HomeView.vue` (or similar) |
|||
|
|||
#### 1. Add DailyNotification Initialization (in mounted lifecycle) |
|||
|
|||
```typescript |
|||
export default defineComponent({ |
|||
name: 'HomeView', |
|||
|
|||
mixins: [PlatformServiceMixin], |
|||
|
|||
async mounted() { |
|||
// ADD THIS: Initialize DailyNotification (only on Capacitor) |
|||
if (this.isCapacitor) { |
|||
await this.$initializeDailyNotification(); |
|||
} |
|||
|
|||
// ... existing mounted code ... |
|||
}, |
|||
|
|||
methods: { |
|||
// ... existing methods ... |
|||
|
|||
/** |
|||
* Enhanced loadNewStarredProjectChanges method |
|||
*/ |
|||
async loadNewStarredProjectChanges(): Promise<void> { |
|||
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(); |
|||
} |
|||
} |
|||
} |
|||
}); |
|||
``` |
|||
|
|||
## 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 |
|||
- Add new methods for plugin functionality |
|||
- Modify existing `initializeDatabase` method |
|||
|
|||
2. **`src/utils/PlatformServiceMixin.ts`** |
|||
- Add new methods for plugin integration |
|||
- Add platform detection for plugin usage |
|||
|
|||
3. **Vue Components (e.g., `src/views/HomeView.vue`)** |
|||
- Add plugin initialization in mounted lifecycle |
|||
- Add enhanced methods for plugin functionality |
|||
|
|||
4. **`package.json`** |
|||
- Add DailyNotification plugin dependency |
|||
|
|||
### Key Benefits: |
|||
- **Same Interface**: Existing methods work exactly the same |
|||
- **Enhanced Functionality**: Background fetching, structured logging, error handling |
|||
- **Platform Detection**: Only initializes on Capacitor platforms |
|||
- **Gradual Migration**: Can be adopted incrementally |
|||
- **No Breaking Changes**: Existing code continues to work |
|||
|
|||
### Migration Strategy: |
|||
1. **Add the changes** to existing TimeSafari PWA code |
|||
2. **Test on Capacitor platforms** (Android, iOS) |
|||
3. **Verify web fallback** works 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 architecture.** |
@ -1,282 +0,0 @@ |
|||
# DailyNotification Architecture Diagram |
|||
|
|||
**Author**: Matthew Raymer |
|||
**Version**: 1.0.0 |
|||
**Created**: 2025-10-08 06:24:57 UTC |
|||
|
|||
## Architecture Overview |
|||
|
|||
This diagram shows how the DailyNotification plugin integrates with your existing TimeSafari PWA code. |
|||
|
|||
``` |
|||
┌─────────────────────────────────────────────────────────────────────────────────┐ |
|||
│ TimeSafari PWA (Your Existing Code) │ |
|||
├─────────────────────────────────────────────────────────────────────────────────┤ |
|||
│ │ |
|||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ |
|||
│ │ HomeView.vue │ │ Your Data │ │ Your Methods │ │ |
|||
│ │ │ │ │ │ │ │ |
|||
│ │ • activeDid │ │ • starredPlan │ │ • loadNewStarred│ │ |
|||
│ │ • axios │ │ HandleIds │ │ ProjectChanges│ │ |
|||
│ │ • apiServer │ │ • lastAckedJwt │ │ • getStarred │ │ |
|||
│ │ • numNewStarred │ │ • numNewStarred │ │ ProjectsWith │ │ |
|||
│ │ ProjectChanges│ │ ProjectChanges│ │ Changes │ │ |
|||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ |
|||
│ │ │ │ │ |
|||
│ └───────────────────────┼───────────────────────┘ │ |
|||
│ │ │ |
|||
│ ▼ │ |
|||
│ ┌─────────────────────────────────────────────────────────────────────────┐ │ |
|||
│ │ DailyNotification Plugin │ │ |
|||
│ ├─────────────────────────────────────────────────────────────────────────┤ │ |
|||
│ │ │ │ |
|||
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ |
|||
│ │ │ Configuration │ │ Integration │ │ Enhanced │ │ │ |
|||
│ │ │ │ │ Service │ │ Methods │ │ │ |
|||
│ │ │ • timesafari │ │ │ │ │ │ │ |
|||
│ │ │ Config │ │ • Uses your │ │ • Same interface│ │ │ |
|||
│ │ │ • networkConfig │ │ existing │ │ • Enhanced │ │ │ |
|||
│ │ │ • contentFetch │ │ axios & data │ │ functionality │ │ │ |
|||
│ │ │ • authentication│ │ • Provides │ │ • Better error │ │ │ |
|||
│ │ │ • logging │ │ enhanced │ │ handling │ │ │ |
|||
│ │ │ • security │ │ methods │ │ • Performance │ │ │ |
|||
│ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ |
|||
│ │ │ │ │ │ │ |
|||
│ │ └───────────────────────┼───────────────────────┘ │ │ |
|||
│ │ │ │ │ |
|||
│ │ ▼ │ │ |
|||
│ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ |
|||
│ │ │ Plugin Features │ │ │ |
|||
│ │ ├─────────────────────────────────────────────────────────────────┤ │ │ |
|||
│ │ │ │ │ │ |
|||
│ │ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ │ |
|||
│ │ │ │ Background │ │ Observability │ │ Performance │ │ │ │ |
|||
│ │ │ │ Fetching │ │ │ │ Optimization │ │ │ │ |
|||
│ │ │ │ │ │ • Structured │ │ │ │ │ │ |
|||
│ │ │ │ • Daily schedule│ │ logging │ │ • HTTP ETags │ │ │ │ |
|||
│ │ │ │ • Notifications │ │ • Event IDs │ │ • Request │ │ │ │ |
|||
│ │ │ │ • Fallback │ │ • Metrics │ │ batching │ │ │ │ |
|||
│ │ │ │ content │ │ • Health checks │ │ • Connection │ │ │ │ |
|||
│ │ │ │ • Offline │ │ • Error │ │ pooling │ │ │ │ |
|||
│ │ │ │ support │ │ tracking │ │ • Caching │ │ │ │ |
|||
│ │ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ │ |
|||
│ │ └─────────────────────────────────────────────────────────────────┘ │ │ |
|||
│ └─────────────────────────────────────────────────────────────────────────┘ │ |
|||
│ │ │ |
|||
│ ▼ │ |
|||
│ ┌─────────────────────────────────────────────────────────────────────────┐ │ |
|||
│ │ External APIs │ │ |
|||
│ ├─────────────────────────────────────────────────────────────────────────┤ │ |
|||
│ │ │ │ |
|||
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ |
|||
│ │ │ Endorser.ch │ │ TimeSafari │ │ Other APIs │ │ │ |
|||
│ │ │ │ │ APIs │ │ │ │ │ |
|||
│ │ │ • /api/v2/ │ │ │ │ • Analytics │ │ │ |
|||
│ │ │ offers/person │ │ • Community │ │ • Monitoring │ │ │ |
|||
│ │ │ • /api/v2/ │ │ features │ │ • Notifications │ │ │ |
|||
│ │ │ offers/plans │ │ • User data │ │ • Storage │ │ │ |
|||
│ │ │ • /api/v2/ │ │ • Preferences │ │ • Backup │ │ │ |
|||
│ │ │ report/ │ │ • Settings │ │ • Sync │ │ │ |
|||
│ │ │ plansLast │ │ │ │ │ │ │ |
|||
│ │ │ UpdatedBetween│ │ │ │ │ │ │ |
|||
│ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ |
|||
│ └─────────────────────────────────────────────────────────────────────────┘ │ |
|||
└─────────────────────────────────────────────────────────────────────────────────┘ |
|||
``` |
|||
|
|||
## Data Flow Diagram |
|||
|
|||
This diagram shows how data flows through the system. |
|||
|
|||
``` |
|||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ |
|||
│ TimeSafari │ │ DailyNotification │ │ Integration │ │ External │ |
|||
│ PWA (You) │ │ Plugin │ │ Service │ │ APIs │ |
|||
│ │ │ │ │ │ │ │ |
|||
│ 1. Call method │───▶│ 2. Configure │───▶│ 3. Enhanced │───▶│ 4. HTTP │ |
|||
│ loadNewStarred│ │ plugin with │ │ method │ │ request │ |
|||
│ ProjectChanges│ │ your data │ │ getStarred │ │ to API │ |
|||
│ │ │ │ │ ProjectsWith │ │ │ |
|||
│ │ │ │ │ Changes │ │ │ |
|||
│ │ │ │ │ │ │ │ |
|||
│ 8. Update UI │◀───│ 7. Return │◀───│ 6. Process │◀───│ 5. HTTP │ |
|||
│ with results │ │ results │ │ response │ │ response │ |
|||
│ │ │ │ │ │ │ │ |
|||
└─────────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘ |
|||
``` |
|||
|
|||
## Method Comparison Diagram |
|||
|
|||
This diagram shows the difference between your existing method and the plugin-enhanced version. |
|||
|
|||
``` |
|||
┌─────────────────────────────────────────────────────────────────────────────────┐ |
|||
│ Your Existing Method │ |
|||
├─────────────────────────────────────────────────────────────────────────────────┤ |
|||
│ │ |
|||
│ async loadNewStarredProjectChanges() { │ |
|||
│ if (this.activeDid && this.starredPlanHandleIds.length > 0) { │ |
|||
│ try { │ |
|||
│ const starredProjectChanges = await getStarredProjectsWithChanges( │ |
|||
│ this.axios, // Your existing axios │ |
|||
│ this.apiServer, // Your existing API server │ |
|||
│ this.activeDid, // Your existing activeDid │ |
|||
│ this.starredPlanHandleIds, // Your existing starred plan IDs │ |
|||
│ this.lastAckedStarredPlanChangesJwtId // Your existing JWT ID │ |
|||
│ ); │ |
|||
│ this.numNewStarredProjectChanges = starredProjectChanges.data.length; │ |
|||
│ this.newStarredProjectChangesHitLimit = starredProjectChanges.hitLimit; │ |
|||
│ } catch (error) { │ |
|||
│ console.warn('[HomeView] Failed to load starred project changes:', error);│ |
|||
│ this.numNewStarredProjectChanges = 0; │ |
|||
│ this.newStarredProjectChangesHitLimit = false; │ |
|||
│ } │ |
|||
│ } else { │ |
|||
│ this.numNewStarredProjectChanges = 0; │ |
|||
│ this.newStarredProjectChangesHitLimit = false; │ |
|||
│ } │ |
|||
│ } │ |
|||
│ │ |
|||
└─────────────────────────────────────────────────────────────────────────────────┘ |
|||
│ |
|||
▼ |
|||
┌─────────────────────────────────────────────────────────────────────────────────┐ |
|||
│ Plugin-Enhanced Method │ |
|||
├─────────────────────────────────────────────────────────────────────────────────┤ |
|||
│ │ |
|||
│ async loadNewStarredProjectChanges() { │ |
|||
│ if (this.activeDid && this.starredPlanHandleIds.length > 0) { │ |
|||
│ try { │ |
|||
│ // SAME INTERFACE - Enhanced functionality │ |
|||
│ const starredProjectChanges = await this.integrationService │ |
|||
│ .getStarredProjectsWithChanges( │ |
|||
│ this.activeDid, // Same parameter │ |
|||
│ this.starredPlanHandleIds, // Same parameter │ |
|||
│ this.lastAckedStarredPlanChangesJwtId // Same parameter │ |
|||
│ ); │ |
|||
│ // SAME HANDLING - Enhanced logging │ |
|||
│ this.numNewStarredProjectChanges = starredProjectChanges.data.length; │ |
|||
│ this.newStarredProjectChangesHitLimit = starredProjectChanges.hitLimit; │ |
|||
│ } catch (error) { │ |
|||
│ // SAME ERROR HANDLING - Enhanced logging │ |
|||
│ console.warn('[HomeView] Failed to load starred project changes:', error);│ |
|||
│ this.numNewStarredProjectChanges = 0; │ |
|||
│ this.newStarredProjectChangesHitLimit = false; │ |
|||
│ } │ |
|||
│ } else { │ |
|||
│ this.numNewStarredProjectChanges = 0; │ |
|||
│ this.newStarredProjectChangesHitLimit = false; │ |
|||
│ } │ |
|||
│ } │ |
|||
│ │ |
|||
│ ✅ SAME INTERFACE ✅ SAME BEHAVIOR ✅ SAME ERROR HANDLING │ |
|||
│ ✅ ENHANCED FEATURES ✅ BETTER LOGGING ✅ PERFORMANCE IMPROVEMENTS │ |
|||
│ │ |
|||
└─────────────────────────────────────────────────────────────────────────────────┘ |
|||
``` |
|||
|
|||
## Configuration Flow Diagram |
|||
|
|||
This diagram shows how the plugin configuration works. |
|||
|
|||
``` |
|||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ |
|||
│ Your Existing │ │ Plugin │ │ Integration │ │ Enhanced │ |
|||
│ TimeSafari │ │ Configuration │ │ Service │ │ Functionality │ |
|||
│ Data │ │ │ │ │ │ │ |
|||
│ │ │ │ │ │ │ │ |
|||
│ • activeDid │───▶│ • timesafari │───▶│ • Uses your │───▶│ • Background │ |
|||
│ • axios │ │ Config │ │ existing │ │ fetching │ |
|||
│ • apiServer │ │ • networkConfig │ │ data & │ │ • Notifications │ |
|||
│ • starredPlan │ │ • contentFetch │ │ methods │ │ • Observability │ |
|||
│ HandleIds │ │ • authentication│ │ • Provides │ │ • Performance │ |
|||
│ • lastAckedJwt │ │ • logging │ │ enhanced │ │ optimization │ |
|||
│ • numNewStarred │ │ • security │ │ methods │ │ • Error │ |
|||
│ ProjectChanges│ │ │ │ • Same │ │ handling │ |
|||
│ │ │ │ │ interface │ │ • Caching │ |
|||
│ │ │ │ │ │ │ • Retry logic │ |
|||
└─────────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘ |
|||
``` |
|||
|
|||
## Benefits Diagram |
|||
|
|||
This diagram shows the benefits of using the plugin. |
|||
|
|||
``` |
|||
┌─────────────────────────────────────────────────────────────────────────────────┐ |
|||
│ Your Existing Code │ |
|||
├─────────────────────────────────────────────────────────────────────────────────┤ |
|||
│ │ |
|||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ |
|||
│ │ Basic │ │ Manual │ │ Limited │ │ |
|||
│ │ Functionality │ │ Error │ │ Observability │ │ |
|||
│ │ │ │ Handling │ │ │ │ |
|||
│ │ • HTTP requests │ │ │ │ • Console logs │ │ |
|||
│ │ • Data handling │ │ • Try/catch │ │ • Basic error │ │ |
|||
│ │ • UI updates │ │ blocks │ │ messages │ │ |
|||
│ │ • Error logging │ │ • Manual retry │ │ • No metrics │ │ |
|||
│ │ │ │ logic │ │ • No monitoring │ │ |
|||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ |
|||
│ │ |
|||
└─────────────────────────────────────────────────────────────────────────────────┘ |
|||
│ |
|||
▼ |
|||
┌─────────────────────────────────────────────────────────────────────────────────┐ |
|||
│ Plugin-Enhanced Code │ |
|||
├─────────────────────────────────────────────────────────────────────────────────┤ |
|||
│ │ |
|||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ |
|||
│ │ Enhanced │ │ Advanced │ │ Comprehensive │ │ |
|||
│ │ Functionality │ │ Error │ │ Observability │ │ |
|||
│ │ │ │ Handling │ │ │ │ |
|||
│ │ • HTTP requests │ │ │ │ • Structured │ │ |
|||
│ │ • Data handling │ │ • Circuit │ │ logging │ │ |
|||
│ │ • UI updates │ │ breaker │ │ • Event IDs │ │ |
|||
│ │ • Background │ │ • Exponential │ │ • Metrics │ │ |
|||
│ │ fetching │ │ backoff │ │ • Health checks │ │ |
|||
│ │ • Notifications │ │ • Retry logic │ │ • Performance │ │ |
|||
│ │ • Caching │ │ • Fallback │ │ monitoring │ │ |
|||
│ │ • Performance │ │ content │ │ • Error │ │ |
|||
│ │ optimization │ │ • Rate limiting │ │ tracking │ │ |
|||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ |
|||
│ │ |
|||
│ ✅ SAME INTERFACE ✅ ENHANCED FEATURES ✅ BETTER OBSERVABILITY │ |
|||
│ ✅ SAME BEHAVIOR ✅ PERFORMANCE GAINS ✅ COMPREHENSIVE MONITORING │ |
|||
│ │ |
|||
└─────────────────────────────────────────────────────────────────────────────────┘ |
|||
``` |
|||
|
|||
## Migration Path Diagram |
|||
|
|||
This diagram shows how to migrate from your existing code to the plugin-enhanced version. |
|||
|
|||
``` |
|||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ |
|||
│ Phase 1: │ │ Phase 2: │ │ Phase 3: │ │ Phase 4: │ |
|||
│ Parallel │ │ Gradual │ │ Full │ │ Optimization │ |
|||
│ Implementation│ │ Migration │ │ Integration │ │ │ |
|||
│ │ │ │ │ │ │ │ |
|||
│ • Keep existing │ │ • Replace │ │ • Replace all │ │ • Leverage │ |
|||
│ code unchanged│ │ methods one │ │ methods with │ │ advanced │ |
|||
│ • Add plugin │ │ by one │ │ plugin │ │ features │ |
|||
│ configuration │ │ • Use enhanced │ │ versions │ │ • Optimize │ |
|||
│ • Test both │ │ error │ │ • Remove │ │ performance │ |
|||
│ implementations│ │ handling │ │ duplicate │ │ • Add advanced │ |
|||
│ • Compare │ │ • Maintain │ │ code │ │ monitoring │ |
|||
│ results │ │ existing UI │ │ • Leverage │ │ • Implement │ |
|||
│ │ │ • Add plugin │ │ plugin │ │ advanced │ |
|||
│ │ │ features │ │ features │ │ features │ |
|||
│ │ │ gradually │ │ │ │ │ |
|||
└─────────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘ |
|||
``` |
|||
|
|||
## Key Takeaways |
|||
|
|||
1. **Same Interface**: The plugin maintains the same interface as your existing methods |
|||
2. **Enhanced Functionality**: Adds background fetching, notifications, and performance optimizations |
|||
3. **Better Error Handling**: Provides circuit breaker, retry logic, and fallback content |
|||
4. **Comprehensive Observability**: Structured logging, metrics, and health checks |
|||
5. **Gradual Migration**: Can be adopted incrementally without breaking existing functionality |
|||
6. **Performance Improvements**: HTTP ETags, request batching, connection pooling, and caching |
|||
|
|||
The plugin enhances your existing TimeSafari PWA code while maintaining the same interface and behavior, making it easy to adopt and migrate to. |
@ -1,542 +0,0 @@ |
|||
# DailyNotification Setup Explanation - Detailed Breakdown |
|||
|
|||
**Author**: Matthew Raymer |
|||
**Version**: 1.0.0 |
|||
**Created**: 2025-10-08 06:24:57 UTC |
|||
|
|||
## Overview |
|||
|
|||
This document provides a detailed explanation of the DailyNotification setup example, breaking down each component and explaining how it integrates with your existing TimeSafari PWA code. |
|||
|
|||
## File Structure Breakdown |
|||
|
|||
### 1. Interface Definitions (Lines 16-28) |
|||
|
|||
```typescript |
|||
// Your existing TimeSafari PWA interfaces |
|||
interface PlanSummaryAndPreviousClaim { |
|||
id: string; |
|||
title: string; |
|||
description: string; |
|||
lastUpdated: string; |
|||
previousClaim?: unknown; |
|||
} |
|||
|
|||
interface StarredProjectsResponse { |
|||
data: Array<PlanSummaryAndPreviousClaim>; |
|||
hitLimit: boolean; |
|||
} |
|||
``` |
|||
|
|||
**What this means:** |
|||
- These are the **exact same interfaces** you already use in your TimeSafari PWA |
|||
- `PlanSummaryAndPreviousClaim` represents a single project with its changes |
|||
- `StarredProjectsResponse` is the response format from your `getStarredProjectsWithChanges()` function |
|||
- The plugin uses these same interfaces to maintain compatibility with your existing code |
|||
|
|||
### 2. TimeSafariHomeView Class (Lines 31-47) |
|||
|
|||
```typescript |
|||
class TimeSafariHomeView { |
|||
// Your existing properties |
|||
activeDid: string = ''; |
|||
starredPlanHandleIds: string[] = []; |
|||
lastAckedStarredPlanChangesJwtId: string = ''; |
|||
numNewStarredProjectChanges: number = 0; |
|||
newStarredProjectChangesHitLimit: boolean = false; |
|||
apiServer: string = 'https://endorser.ch'; |
|||
axios: AxiosInstance; |
|||
|
|||
// Plugin integration |
|||
private dailyNotificationService: DailyNotification | null = null; |
|||
private integrationService: TimeSafariIntegrationService | null = null; |
|||
} |
|||
``` |
|||
|
|||
**What this means:** |
|||
- This represents your **existing TimeSafari PWA HomeView component** |
|||
- All the properties (activeDid, starredPlanHandleIds, etc.) are your **existing data** |
|||
- The plugin integration properties are **new additions** that work alongside your existing code |
|||
- The plugin doesn't replace your existing properties - it enhances them |
|||
|
|||
## Core Setup Method Breakdown |
|||
|
|||
### 3. setupDailyNotification() Method (Lines 52-223) |
|||
|
|||
This is the **main configuration method** that sets up the plugin to work with your existing TimeSafari code. |
|||
|
|||
#### Step 1: Basic Plugin Configuration (Lines 58-63) |
|||
|
|||
```typescript |
|||
await DailyNotification.configure({ |
|||
// Basic plugin configuration |
|||
storage: 'tiered', |
|||
ttlSeconds: 1800, |
|||
enableETagSupport: true, |
|||
enableErrorHandling: true, |
|||
enablePerformanceOptimization: true, |
|||
``` |
|||
|
|||
**What this means:** |
|||
- `storage: 'tiered'` - Uses a multi-level storage system (memory + disk + network) |
|||
- `ttlSeconds: 1800` - Data expires after 30 minutes (1800 seconds) |
|||
- `enableETagSupport: true` - Uses HTTP ETags for efficient caching (only fetch if data changed) |
|||
- `enableErrorHandling: true` - Built-in retry logic and error recovery |
|||
- `enablePerformanceOptimization: true` - Automatic performance optimizations |
|||
|
|||
#### Step 2: TimeSafari-Specific Configuration (Lines 66-103) |
|||
|
|||
```typescript |
|||
timesafariConfig: { |
|||
// Required: Your existing activeDid |
|||
activeDid: this.activeDid, |
|||
|
|||
// Your existing API endpoints |
|||
endpoints: { |
|||
offersToPerson: `${this.apiServer}/api/v2/offers/person`, |
|||
offersToPlans: `${this.apiServer}/api/v2/offers/plans`, |
|||
projectsLastUpdated: `${this.apiServer}/api/v2/report/plansLastUpdatedBetween` |
|||
}, |
|||
|
|||
// Configure starred projects fetching (matches your existing pattern) |
|||
starredProjectsConfig: { |
|||
enabled: true, |
|||
starredPlanHandleIds: this.starredPlanHandleIds, |
|||
lastAckedJwtId: this.lastAckedStarredPlanChangesJwtId, |
|||
fetchInterval: '0 8 * * *', // Daily at 8 AM |
|||
maxResults: 50, |
|||
hitLimitHandling: 'warn' |
|||
} |
|||
} |
|||
``` |
|||
|
|||
**What this means:** |
|||
- `activeDid: this.activeDid` - Uses your **existing user DID** for authentication |
|||
- `endpoints` - Points to your **existing TimeSafari API endpoints** |
|||
- `starredProjectsConfig` - Configures the plugin to fetch starred projects **exactly like your existing code** |
|||
- `starredPlanHandleIds: this.starredPlanHandleIds` - Uses your existing array of starred project IDs |
|||
- `lastAckedJwtId: this.lastAckedStarredPlanChangesJwtId` - Uses your existing JWT ID for pagination |
|||
- `fetchInterval: '0 8 * * *'` - Fetches daily at 8 AM (cron expression) |
|||
- `hitLimitHandling: 'warn'` - Same error handling as your existing code |
|||
|
|||
#### Step 3: Network Configuration (Lines 106-121) |
|||
|
|||
```typescript |
|||
networkConfig: { |
|||
// Use your existing axios instance |
|||
httpClient: this.axios, |
|||
baseURL: this.apiServer, |
|||
timeout: 30000, |
|||
retryAttempts: 3, |
|||
retryDelay: 1000, |
|||
maxConcurrent: 5, |
|||
|
|||
// Headers matching your existing pattern |
|||
defaultHeaders: { |
|||
'Content-Type': 'application/json', |
|||
'Accept': 'application/json', |
|||
'User-Agent': 'TimeSafari-PWA/1.0.0' |
|||
} |
|||
} |
|||
``` |
|||
|
|||
**What this means:** |
|||
- `httpClient: this.axios` - Uses your **existing axios instance** (no new HTTP client needed) |
|||
- `baseURL: this.apiServer` - Uses your **existing API server URL** |
|||
- `timeout: 30000` - 30-second timeout for requests |
|||
- `retryAttempts: 3` - Retry failed requests up to 3 times |
|||
- `retryDelay: 1000` - Wait 1 second between retries |
|||
- `maxConcurrent: 5` - Maximum 5 concurrent requests |
|||
- `defaultHeaders` - Headers that match your existing request pattern |
|||
|
|||
#### Step 4: Content Fetch Configuration (Lines 124-149) |
|||
|
|||
```typescript |
|||
contentFetch: { |
|||
enabled: true, |
|||
schedule: '0 8 * * *', // Daily at 8 AM |
|||
|
|||
// Your existing request pattern |
|||
requestConfig: { |
|||
method: 'POST', |
|||
url: `${this.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 your existing error handling |
|||
callbacks: { |
|||
onSuccess: this.handleStarredProjectsSuccess.bind(this), |
|||
onError: this.handleStarredProjectsError.bind(this), |
|||
onComplete: this.handleStarredProjectsComplete.bind(this) |
|||
} |
|||
} |
|||
``` |
|||
|
|||
**What this means:** |
|||
- `contentFetch` - Configures the plugin to fetch content **exactly like your existing `getStarredProjectsWithChanges()` function** |
|||
- `requestConfig` - Defines the HTTP request that matches your existing API call: |
|||
- `method: 'POST'` - Same as your existing function |
|||
- `url` - Same endpoint as your existing function |
|||
- `headers` - Same headers as your existing function (with template variables) |
|||
- `body` - Same request body as your existing function (with template variables) |
|||
- `callbacks` - Functions that handle the response **exactly like your existing error handling** |
|||
|
|||
#### Step 5: Authentication Configuration (Lines 152-159) |
|||
|
|||
```typescript |
|||
authentication: { |
|||
jwt: { |
|||
secret: process.env.JWT_SECRET || 'your-jwt-secret', |
|||
algorithm: 'HS256', |
|||
expirationMinutes: 60, |
|||
refreshThresholdMinutes: 10 |
|||
} |
|||
} |
|||
``` |
|||
|
|||
**What this means:** |
|||
- Configures JWT authentication for API requests |
|||
- Uses the same JWT secret as your existing TimeSafari PWA |
|||
- Tokens expire after 60 minutes and refresh when 10 minutes remain |
|||
- This ensures the plugin uses the same authentication as your existing code |
|||
|
|||
#### Step 6: Observability Configuration (Lines 162-168) |
|||
|
|||
```typescript |
|||
logging: { |
|||
level: 'INFO', |
|||
enableRequestLogging: true, |
|||
enableResponseLogging: true, |
|||
enableErrorLogging: true, |
|||
redactSensitiveData: true |
|||
} |
|||
``` |
|||
|
|||
**What this means:** |
|||
- `level: 'INFO'` - Logs informational messages and above |
|||
- `enableRequestLogging: true` - Logs all HTTP requests (useful for debugging) |
|||
- `enableResponseLogging: true` - Logs all HTTP responses |
|||
- `enableErrorLogging: true` - Logs all errors with detailed information |
|||
- `redactSensitiveData: true` - Automatically removes sensitive data from logs |
|||
|
|||
#### Step 7: Security Configuration (Lines 171-181) |
|||
|
|||
```typescript |
|||
security: { |
|||
certificatePinning: { |
|||
enabled: true, |
|||
pins: [ |
|||
{ |
|||
hostname: 'endorser.ch', |
|||
pins: ['sha256/YOUR_PIN_HERE'] |
|||
} |
|||
] |
|||
} |
|||
} |
|||
``` |
|||
|
|||
**What this means:** |
|||
- `certificatePinning` - Prevents man-in-the-middle attacks by pinning SSL certificates |
|||
- Only allows connections to `endorser.ch` with the specified certificate |
|||
- This adds an extra layer of security to your API requests |
|||
|
|||
## Integration Service Setup (Lines 185-207) |
|||
|
|||
```typescript |
|||
// Step 2: Initialize TimeSafari Integration Service |
|||
this.integrationService = TimeSafariIntegrationService.getInstance(); |
|||
await this.integrationService.initialize({ |
|||
activeDid: this.activeDid, |
|||
storageAdapter: this.getTimeSafariStorageAdapter(), |
|||
endorserApiBaseUrl: this.apiServer, |
|||
|
|||
// Use your existing request patterns |
|||
requestConfig: { |
|||
httpClient: this.axios, |
|||
baseURL: this.apiServer, |
|||
timeout: 30000, |
|||
retryAttempts: 3 |
|||
}, |
|||
|
|||
// Configure starred projects fetching |
|||
starredProjectsConfig: { |
|||
enabled: true, |
|||
starredPlanHandleIds: this.starredPlanHandleIds, |
|||
lastAckedJwtId: this.lastAckedStarredPlanChangesJwtId, |
|||
fetchInterval: '0 8 * * *', |
|||
maxResults: 50 |
|||
} |
|||
}); |
|||
``` |
|||
|
|||
**What this means:** |
|||
- `TimeSafariIntegrationService` - A service that provides **enhanced versions** of your existing TimeSafari functions |
|||
- `getInstance()` - Uses the singleton pattern (same instance across your app) |
|||
- `initialize()` - Sets up the service with your existing TimeSafari configuration |
|||
- `storageAdapter: this.getTimeSafariStorageAdapter()` - Uses your existing storage system |
|||
- The service provides methods that work **exactly like your existing functions** but with enhanced features |
|||
|
|||
## Enhanced Method Breakdown |
|||
|
|||
### 4. loadNewStarredProjectChanges() Method (Lines 231-262) |
|||
|
|||
```typescript |
|||
async loadNewStarredProjectChanges(): Promise<void> { |
|||
if (this.activeDid && this.starredPlanHandleIds.length > 0) { |
|||
try { |
|||
// Use plugin's enhanced fetching with same interface as your existing code |
|||
const starredProjectChanges = await this.integrationService!.getStarredProjectsWithChanges( |
|||
this.activeDid, |
|||
this.starredPlanHandleIds, |
|||
this.lastAckedStarredPlanChangesJwtId |
|||
); |
|||
|
|||
// Same handling as your existing code |
|||
this.numNewStarredProjectChanges = starredProjectChanges.data.length; |
|||
this.newStarredProjectChangesHitLimit = starredProjectChanges.hitLimit; |
|||
|
|||
} catch (error) { |
|||
// Same error handling as your existing code |
|||
console.warn('[HomeView] Failed to load starred project changes:', error); |
|||
this.numNewStarredProjectChanges = 0; |
|||
this.newStarredProjectChangesHitLimit = false; |
|||
} |
|||
} else { |
|||
this.numNewStarredProjectChanges = 0; |
|||
this.newStarredProjectChangesHitLimit = false; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
**What this means:** |
|||
- This is an **enhanced version** of your existing `loadNewStarredProjectChanges()` method |
|||
- The **interface is identical** - same parameters, same return values, same error handling |
|||
- The **behavior is identical** - same logic, same data handling, same UI updates |
|||
- The **only difference** is that it uses the plugin's enhanced `getStarredProjectsWithChanges()` method |
|||
- Your existing code that calls this method **doesn't need to change at all** |
|||
|
|||
## Callback Handlers Breakdown |
|||
|
|||
### 5. Success Handler (Lines 267-280) |
|||
|
|||
```typescript |
|||
async handleStarredProjectsSuccess(data: StarredProjectsResponse): Promise<void> { |
|||
// Same handling as your existing code |
|||
this.numNewStarredProjectChanges = data.data.length; |
|||
this.newStarredProjectChangesHitLimit = data.hitLimit; |
|||
|
|||
// Update UI (your existing method) |
|||
this.updateStarredProjectsUI(data); |
|||
|
|||
// Enhanced logging (optional) |
|||
console.log('Starred projects success callback:', { |
|||
count: data.data.length, |
|||
hitLimit: data.hitLimit |
|||
}); |
|||
} |
|||
``` |
|||
|
|||
**What this means:** |
|||
- This handler is called when the starred projects fetch **succeeds** |
|||
- It does **exactly the same thing** as your existing success handling |
|||
- It updates your existing properties (`numNewStarredProjectChanges`, `newStarredProjectChangesHitLimit`) |
|||
- It calls your existing UI update method (`updateStarredProjectsUI`) |
|||
- It adds optional enhanced logging for debugging |
|||
|
|||
### 6. Error Handler (Lines 282-294) |
|||
|
|||
```typescript |
|||
async handleStarredProjectsError(error: Error): Promise<void> { |
|||
// Same error handling as your existing code |
|||
console.warn('[HomeView] Failed to load starred project changes:', error); |
|||
this.numNewStarredProjectChanges = 0; |
|||
this.newStarredProjectChangesHitLimit = false; |
|||
|
|||
// Enhanced error handling (optional) |
|||
console.error('Starred projects error callback:', { |
|||
error: error.message, |
|||
activeDid: this.activeDid, |
|||
planCount: this.starredPlanHandleIds.length |
|||
}); |
|||
} |
|||
``` |
|||
|
|||
**What this means:** |
|||
- This handler is called when the starred projects fetch **fails** |
|||
- It does **exactly the same thing** as your existing error handling |
|||
- It logs the same warning message as your existing code |
|||
- It resets your existing properties to the same default values |
|||
- It adds optional enhanced error logging with more context |
|||
|
|||
## Vue.js Integration Breakdown |
|||
|
|||
### 7. Vue.js Mixin (Lines 349-463) |
|||
|
|||
```typescript |
|||
export const TimeSafariDailyNotificationMixin = { |
|||
data() { |
|||
return { |
|||
// Your existing data |
|||
activeDid: '', |
|||
starredPlanHandleIds: [] as string[], |
|||
lastAckedStarredPlanChangesJwtId: '', |
|||
numNewStarredProjectChanges: 0, |
|||
newStarredProjectChangesHitLimit: false, |
|||
|
|||
// Plugin integration |
|||
dailyNotificationService: null as DailyNotification | null, |
|||
integrationService: null as TimeSafariIntegrationService | null |
|||
}; |
|||
}, |
|||
|
|||
async mounted() { |
|||
// Setup DailyNotification when component mounts |
|||
await this.setupDailyNotification(); |
|||
}, |
|||
|
|||
methods: { |
|||
// ... methods |
|||
} |
|||
}; |
|||
``` |
|||
|
|||
**What this means:** |
|||
- This is a **Vue.js mixin** that you can add to your existing TimeSafari PWA components |
|||
- It adds the plugin functionality to your existing Vue components |
|||
- It preserves all your existing data properties |
|||
- It adds the plugin setup to the `mounted()` lifecycle hook |
|||
- It provides all the enhanced methods as component methods |
|||
|
|||
## Usage Example Breakdown |
|||
|
|||
### 8. Usage Function (Lines 318-346) |
|||
|
|||
```typescript |
|||
export async function setupDailyNotificationForTimeSafari( |
|||
axiosInstance: AxiosInstance, |
|||
activeDid: string, |
|||
starredPlanHandleIds: string[], |
|||
lastAckedJwtId: string, |
|||
apiServer: string = 'https://endorser.ch' |
|||
): Promise<TimeSafariHomeView> { |
|||
|
|||
console.log('Setting up DailyNotification for TimeSafari PWA...'); |
|||
|
|||
// Create your existing HomeView instance |
|||
const homeView = new TimeSafariHomeView(axiosInstance); |
|||
|
|||
// Set up your existing TimeSafari data |
|||
homeView.activeDid = activeDid; |
|||
homeView.starredPlanHandleIds = starredPlanHandleIds; |
|||
homeView.lastAckedStarredPlanChangesJwtId = lastAckedJwtId; |
|||
homeView.apiServer = apiServer; |
|||
|
|||
// Setup DailyNotification plugin |
|||
await homeView.setupDailyNotification(); |
|||
|
|||
// Test the enhanced method |
|||
await homeView.loadNewStarredProjectChanges(); |
|||
|
|||
console.log('DailyNotification setup completed successfully!'); |
|||
|
|||
return homeView; |
|||
} |
|||
``` |
|||
|
|||
**What this means:** |
|||
- This is a **helper function** that sets up the plugin for your TimeSafari PWA |
|||
- It takes your existing TimeSafari data as parameters |
|||
- It creates your existing HomeView instance |
|||
- It sets up your existing data properties |
|||
- It configures the plugin with your existing configuration |
|||
- It tests the enhanced method to ensure everything works |
|||
- It returns your enhanced HomeView instance |
|||
|
|||
## Key Benefits Explained |
|||
|
|||
### 1. **Same Interface, Enhanced Functionality** |
|||
- Your existing `loadNewStarredProjectChanges()` method works exactly the same |
|||
- Your existing `getStarredProjectsWithChanges()` function is enhanced with better error handling |
|||
- Your existing UI code doesn't need to change at all |
|||
- Your existing data properties are preserved and enhanced |
|||
|
|||
### 2. **Enhanced Error Handling** |
|||
- Built-in retry logic for failed requests |
|||
- Exponential backoff for rate limiting |
|||
- Circuit breaker pattern for service failures |
|||
- Structured logging with event IDs for debugging |
|||
|
|||
### 3. **Performance Improvements** |
|||
- HTTP ETag support for efficient caching |
|||
- Request batching for multiple API calls |
|||
- Connection pooling for HTTP requests |
|||
- Automatic performance optimizations |
|||
|
|||
### 4. **Background Fetching** |
|||
- Automatically fetches starred projects in the background |
|||
- Schedules daily notifications with your data |
|||
- Works even when the app is in the background |
|||
- Provides fallback content when network is unavailable |
|||
|
|||
### 5. **Observability** |
|||
- Structured logging with event IDs |
|||
- Performance metrics and monitoring |
|||
- Error tracking and analysis |
|||
- Health checks and status monitoring |
|||
|
|||
## Migration Strategy |
|||
|
|||
### Phase 1: Parallel Implementation |
|||
1. **Keep your existing code unchanged** |
|||
2. **Add the plugin configuration alongside your existing code** |
|||
3. **Test both implementations in parallel** |
|||
4. **Compare results to ensure compatibility** |
|||
|
|||
### Phase 2: Gradual Migration |
|||
1. **Replace individual methods one by one** |
|||
2. **Use the plugin's enhanced error handling** |
|||
3. **Maintain your existing UI and user experience** |
|||
4. **Add plugin-specific features gradually** |
|||
|
|||
### Phase 3: Full Integration |
|||
1. **Replace all TimeSafari request patterns with the plugin** |
|||
2. **Remove duplicate code** |
|||
3. **Leverage the plugin's advanced features** |
|||
4. **Optimize performance with the plugin's caching and batching** |
|||
|
|||
## Common Questions |
|||
|
|||
### Q: Do I need to change my existing code? |
|||
**A:** No, your existing code doesn't need to change. The plugin enhances your existing methods while maintaining the same interface. |
|||
|
|||
### Q: Will this break my existing functionality? |
|||
**A:** No, the plugin is designed to work alongside your existing code. You can test both implementations in parallel. |
|||
|
|||
### Q: How do I know if the plugin is working? |
|||
**A:** The plugin provides enhanced logging and metrics. You can compare the results between your existing code and the plugin-enhanced version. |
|||
|
|||
### Q: Can I roll back if something goes wrong? |
|||
**A:** Yes, you can easily roll back by using your existing methods instead of the plugin-enhanced versions. |
|||
|
|||
### Q: What are the performance benefits? |
|||
**A:** The plugin provides HTTP ETag support, request batching, connection pooling, and automatic performance optimizations. |
|||
|
|||
## Conclusion |
|||
|
|||
The DailyNotification setup example shows how to enhance your existing TimeSafari PWA code with advanced features while maintaining the same interface and behavior. The plugin works alongside your existing code, providing enhanced error handling, performance improvements, background fetching, and observability without requiring changes to your existing methods or UI. |
|||
|
|||
--- |
|||
|
|||
**Next Steps:** |
|||
1. Review the setup example and understand each component |
|||
2. Test the plugin configuration with your existing TimeSafari PWA code |
|||
3. Compare the results between your existing methods and the plugin-enhanced versions |
|||
4. Gradually migrate to the plugin-enhanced methods as you gain confidence |
@ -1,387 +0,0 @@ |
|||
# DailyNotification Setup Guide for TimeSafari PWA |
|||
|
|||
**Author**: Matthew Raymer |
|||
**Version**: 1.0.0 |
|||
**Created**: 2025-10-08 06:24:57 UTC |
|||
|
|||
## Overview |
|||
|
|||
This guide shows you exactly how to set up the DailyNotification plugin in your existing TimeSafari PWA to work with your current `loadNewStarredProjectChanges()` and `getStarredProjectsWithChanges()` methods. |
|||
|
|||
## Step-by-Step Setup |
|||
|
|||
### Step 1: Install the Plugin |
|||
|
|||
```bash |
|||
# In your TimeSafari PWA project |
|||
npm install ssh://git@173.199.124.46:222/trent_larson/daily-notification-plugin.git |
|||
``` |
|||
|
|||
### Step 2: Import the Plugin |
|||
|
|||
```typescript |
|||
// In your TimeSafari PWA file (e.g., HomeView.vue or main.ts) |
|||
import { DailyNotification } from '@timesafari/daily-notification-plugin'; |
|||
import { TimeSafariIntegrationService } from '@timesafari/daily-notification-plugin'; |
|||
``` |
|||
|
|||
### Step 3: Configure the Plugin |
|||
|
|||
Add this configuration to your existing TimeSafari PWA code: |
|||
|
|||
```typescript |
|||
// In your TimeSafari PWA class/component |
|||
async setupDailyNotification() { |
|||
try { |
|||
// Configure the plugin with your existing TimeSafari settings |
|||
await DailyNotification.configure({ |
|||
// Basic configuration |
|||
storage: 'tiered', |
|||
ttlSeconds: 1800, |
|||
enableETagSupport: true, |
|||
|
|||
// TimeSafari-specific configuration |
|||
timesafariConfig: { |
|||
// Your existing activeDid |
|||
activeDid: this.activeDid, |
|||
|
|||
// Your existing API endpoints |
|||
endpoints: { |
|||
offersToPerson: `${this.apiServer}/api/v2/offers/person`, |
|||
offersToPlans: `${this.apiServer}/api/v2/offers/plans`, |
|||
projectsLastUpdated: `${this.apiServer}/api/v2/report/plansLastUpdatedBetween` |
|||
}, |
|||
|
|||
// Configure starred projects fetching (matches your existing pattern) |
|||
starredProjectsConfig: { |
|||
enabled: true, |
|||
starredPlanHandleIds: this.starredPlanHandleIds, |
|||
lastAckedJwtId: this.lastAckedStarredPlanChangesJwtId, |
|||
fetchInterval: '0 8 * * *', // Daily at 8 AM |
|||
maxResults: 50, |
|||
hitLimitHandling: 'warn' |
|||
} |
|||
}, |
|||
|
|||
// Network configuration using your existing axios instance |
|||
networkConfig: { |
|||
httpClient: this.axios, // Your existing axios instance |
|||
baseURL: this.apiServer, // Your existing API server |
|||
timeout: 30000, |
|||
retryAttempts: 3 |
|||
}, |
|||
|
|||
// Content fetch configuration (replaces your existing method) |
|||
contentFetch: { |
|||
enabled: true, |
|||
schedule: '0 8 * * *', // Daily at 8 AM |
|||
|
|||
// Your existing request pattern |
|||
requestConfig: { |
|||
method: 'POST', |
|||
url: `${this.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 your existing error handling |
|||
callbacks: { |
|||
onSuccess: this.handleStarredProjectsSuccess.bind(this), |
|||
onError: this.handleStarredProjectsError.bind(this) |
|||
} |
|||
} |
|||
}); |
|||
|
|||
// Initialize TimeSafari Integration Service |
|||
this.integrationService = TimeSafariIntegrationService.getInstance(); |
|||
await this.integrationService.initialize({ |
|||
activeDid: this.activeDid, |
|||
storageAdapter: this.getTimeSafariStorageAdapter(), |
|||
endorserApiBaseUrl: this.apiServer |
|||
}); |
|||
|
|||
console.log('DailyNotification setup completed successfully!'); |
|||
|
|||
} catch (error) { |
|||
console.error('Failed to setup DailyNotification:', error); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### Step 4: Replace Your Existing Method |
|||
|
|||
Replace your existing `loadNewStarredProjectChanges()` method with this enhanced version: |
|||
|
|||
```typescript |
|||
// Enhanced version of your existing method |
|||
async loadNewStarredProjectChanges() { |
|||
if (this.activeDid && this.starredPlanHandleIds.length > 0) { |
|||
try { |
|||
// Use plugin's enhanced fetching with same interface as your existing code |
|||
const starredProjectChanges = await this.integrationService.getStarredProjectsWithChanges( |
|||
this.activeDid, |
|||
this.starredPlanHandleIds, |
|||
this.lastAckedStarredPlanChangesJwtId |
|||
); |
|||
|
|||
// Same handling as your existing code |
|||
this.numNewStarredProjectChanges = starredProjectChanges.data.length; |
|||
this.newStarredProjectChangesHitLimit = starredProjectChanges.hitLimit; |
|||
|
|||
} catch (error) { |
|||
// Same error handling as your existing code |
|||
console.warn('[HomeView] Failed to load starred project changes:', error); |
|||
this.numNewStarredProjectChanges = 0; |
|||
this.newStarredProjectChangesHitLimit = false; |
|||
} |
|||
} else { |
|||
this.numNewStarredProjectChanges = 0; |
|||
this.newStarredProjectChangesHitLimit = false; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### Step 5: Add Callback Handlers |
|||
|
|||
Add these callback handlers that match your existing error handling patterns: |
|||
|
|||
```typescript |
|||
// Callback handlers that match your existing error handling |
|||
async handleStarredProjectsSuccess(data: { data: Array<PlanSummaryAndPreviousClaim>; hitLimit: boolean }) { |
|||
// Same handling as your existing code |
|||
this.numNewStarredProjectChanges = data.data.length; |
|||
this.newStarredProjectChangesHitLimit = data.hitLimit; |
|||
|
|||
// Update UI (your existing method) |
|||
this.updateStarredProjectsUI(data); |
|||
} |
|||
|
|||
async handleStarredProjectsError(error: Error) { |
|||
// Same error handling as your existing code |
|||
console.warn('[HomeView] Failed to load starred project changes:', error); |
|||
this.numNewStarredProjectChanges = 0; |
|||
this.newStarredProjectChangesHitLimit = false; |
|||
} |
|||
``` |
|||
|
|||
### Step 6: Initialize in Your Component |
|||
|
|||
Add the setup to your existing TimeSafari PWA component: |
|||
|
|||
```typescript |
|||
// In your existing TimeSafari PWA component (e.g., HomeView.vue) |
|||
export default defineComponent({ |
|||
name: 'HomeView', |
|||
|
|||
data() { |
|||
return { |
|||
// Your existing data |
|||
activeDid: '', |
|||
starredPlanHandleIds: [] as string[], |
|||
lastAckedStarredPlanChangesJwtId: '', |
|||
numNewStarredProjectChanges: 0, |
|||
newStarredProjectChangesHitLimit: false, |
|||
|
|||
// Plugin integration |
|||
integrationService: null as TimeSafariIntegrationService | null |
|||
}; |
|||
}, |
|||
|
|||
async mounted() { |
|||
// Setup DailyNotification when component mounts |
|||
await this.setupDailyNotification(); |
|||
}, |
|||
|
|||
methods: { |
|||
// Add the setupDailyNotification method from Step 3 |
|||
async setupDailyNotification() { /* ... */ }, |
|||
|
|||
// Replace your existing loadNewStarredProjectChanges with Step 4 version |
|||
async loadNewStarredProjectChanges() { /* ... */ }, |
|||
|
|||
// Add callback handlers from Step 5 |
|||
async handleStarredProjectsSuccess(data) { /* ... */ }, |
|||
async handleStarredProjectsError(error) { /* ... */ }, |
|||
|
|||
// Your existing methods (unchanged) |
|||
updateStarredProjectsUI(data) { /* ... */ }, |
|||
getTimeSafariStorageAdapter() { /* ... */ } |
|||
} |
|||
}); |
|||
``` |
|||
|
|||
## Complete Example |
|||
|
|||
Here's a complete example showing how to integrate the plugin into your existing TimeSafari PWA: |
|||
|
|||
```typescript |
|||
// Complete integration example |
|||
import { DailyNotification } from '@timesafari/daily-notification-plugin'; |
|||
import { TimeSafariIntegrationService } from '@timesafari/daily-notification-plugin'; |
|||
|
|||
class TimeSafariHomeView { |
|||
// Your existing properties |
|||
activeDid: string = ''; |
|||
starredPlanHandleIds: string[] = []; |
|||
lastAckedStarredPlanChangesJwtId: string = ''; |
|||
numNewStarredProjectChanges: number = 0; |
|||
newStarredProjectChangesHitLimit: boolean = false; |
|||
apiServer: string = 'https://endorser.ch'; |
|||
axios: AxiosInstance; |
|||
|
|||
// Plugin integration |
|||
private integrationService: TimeSafariIntegrationService | null = null; |
|||
|
|||
constructor(axiosInstance: AxiosInstance) { |
|||
this.axios = axiosInstance; |
|||
} |
|||
|
|||
async setupDailyNotification() { |
|||
await DailyNotification.configure({ |
|||
timesafariConfig: { |
|||
activeDid: this.activeDid, |
|||
endpoints: { |
|||
projectsLastUpdated: `${this.apiServer}/api/v2/report/plansLastUpdatedBetween` |
|||
}, |
|||
starredProjectsConfig: { |
|||
enabled: true, |
|||
starredPlanHandleIds: this.starredPlanHandleIds, |
|||
lastAckedJwtId: this.lastAckedStarredPlanChangesJwtId, |
|||
fetchInterval: '0 8 * * *' |
|||
} |
|||
}, |
|||
networkConfig: { |
|||
httpClient: this.axios, |
|||
baseURL: this.apiServer, |
|||
timeout: 30000 |
|||
}, |
|||
contentFetch: { |
|||
enabled: true, |
|||
schedule: '0 8 * * *', |
|||
callbacks: { |
|||
onSuccess: this.handleStarredProjectsSuccess.bind(this), |
|||
onError: this.handleStarredProjectsError.bind(this) |
|||
} |
|||
} |
|||
}); |
|||
|
|||
this.integrationService = TimeSafariIntegrationService.getInstance(); |
|||
await this.integrationService.initialize({ |
|||
activeDid: this.activeDid, |
|||
storageAdapter: this.getTimeSafariStorageAdapter(), |
|||
endorserApiBaseUrl: this.apiServer |
|||
}); |
|||
} |
|||
|
|||
async loadNewStarredProjectChanges() { |
|||
if (this.activeDid && this.starredPlanHandleIds.length > 0) { |
|||
try { |
|||
const starredProjectChanges = await this.integrationService!.getStarredProjectsWithChanges( |
|||
this.activeDid, |
|||
this.starredPlanHandleIds, |
|||
this.lastAckedStarredPlanChangesJwtId |
|||
); |
|||
|
|||
this.numNewStarredProjectChanges = starredProjectChanges.data.length; |
|||
this.newStarredProjectChangesHitLimit = starredProjectChanges.hitLimit; |
|||
|
|||
} catch (error) { |
|||
console.warn('[HomeView] Failed to load starred project changes:', error); |
|||
this.numNewStarredProjectChanges = 0; |
|||
this.newStarredProjectChangesHitLimit = false; |
|||
} |
|||
} else { |
|||
this.numNewStarredProjectChanges = 0; |
|||
this.newStarredProjectChangesHitLimit = false; |
|||
} |
|||
} |
|||
|
|||
async handleStarredProjectsSuccess(data: { data: Array<PlanSummaryAndPreviousClaim>; hitLimit: boolean }) { |
|||
this.numNewStarredProjectChanges = data.data.length; |
|||
this.newStarredProjectChangesHitLimit = data.hitLimit; |
|||
this.updateStarredProjectsUI(data); |
|||
} |
|||
|
|||
async handleStarredProjectsError(error: Error) { |
|||
console.warn('[HomeView] Failed to load starred project changes:', error); |
|||
this.numNewStarredProjectChanges = 0; |
|||
this.newStarredProjectChangesHitLimit = false; |
|||
} |
|||
|
|||
// Your existing methods (unchanged) |
|||
private updateStarredProjectsUI(data: { data: Array<PlanSummaryAndPreviousClaim>; hitLimit: boolean }) { |
|||
// Your existing UI update logic |
|||
} |
|||
|
|||
private getTimeSafariStorageAdapter() { |
|||
// Your existing storage adapter |
|||
return {}; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
## What You Get |
|||
|
|||
After following this setup, you'll have: |
|||
|
|||
✅ **Same Interface**: Your existing `loadNewStarredProjectChanges()` method works exactly the same |
|||
✅ **Enhanced Features**: Background fetching, structured logging, metrics |
|||
✅ **Better Error Handling**: Enhanced error handling with retry logic |
|||
✅ **Performance Improvements**: Caching, batching, and optimization |
|||
✅ **Observability**: Structured logging with event IDs and metrics |
|||
✅ **Background Notifications**: Daily notifications with your starred projects data |
|||
|
|||
## Testing |
|||
|
|||
Test the integration by calling your existing method: |
|||
|
|||
```typescript |
|||
// Test the enhanced method |
|||
await homeView.loadNewStarredProjectChanges(); |
|||
|
|||
// Check the results (same as before) |
|||
console.log('New starred project changes:', homeView.numNewStarredProjectChanges); |
|||
console.log('Hit limit:', homeView.newStarredProjectChangesHitLimit); |
|||
``` |
|||
|
|||
## Troubleshooting |
|||
|
|||
### Common Issues |
|||
|
|||
1. **Plugin not found**: Make sure you've installed the plugin correctly |
|||
2. **Configuration errors**: Check that all required fields are provided |
|||
3. **Network errors**: Verify your axios instance and API server URL |
|||
4. **Authentication errors**: Check your JWT token and activeDid |
|||
|
|||
### Debug Mode |
|||
|
|||
Enable debug logging to troubleshoot issues: |
|||
|
|||
```typescript |
|||
await DailyNotification.configure({ |
|||
logging: { |
|||
level: 'DEBUG', |
|||
enableRequestLogging: true, |
|||
enableResponseLogging: true, |
|||
enableErrorLogging: true |
|||
} |
|||
}); |
|||
``` |
|||
|
|||
## Next Steps |
|||
|
|||
1. **Test the integration** with your existing TimeSafari PWA code |
|||
2. **Compare results** between your existing method and the plugin-enhanced version |
|||
3. **Gradually migrate** other request methods to use the plugin |
|||
4. **Leverage advanced features** like background fetching and observability |
|||
|
|||
--- |
|||
|
|||
**That's it!** Your TimeSafari PWA now has enhanced daily notification capabilities while maintaining the same interface and behavior you're already familiar with. |
@ -1,252 +0,0 @@ |
|||
# DailyNotification Plugin - Usage Decision Guide |
|||
|
|||
**Author**: Matthew Raymer |
|||
**Version**: 1.0.0 |
|||
**Created**: 2025-10-08 06:24:57 UTC |
|||
|
|||
## Quick Decision Flow |
|||
|
|||
``` |
|||
┌─────────────────────────────────────────────────────────────────────────────────┐ |
|||
│ TimeSafari PWA Project │ |
|||
├─────────────────────────────────────────────────────────────────────────────────┤ |
|||
│ │ |
|||
│ ┌─────────────────┐ │ |
|||
│ │ Does your │ │ |
|||
│ │ TimeSafari │ │ |
|||
│ │ PWA use │ │ |
|||
│ │ Capacitor? │ │ |
|||
│ └─────────────────┘ │ |
|||
│ │ │ |
|||
│ ▼ │ |
|||
│ ┌─────────────────┐ ┌─────────────────┐ │ |
|||
│ │ YES │ │ NO │ │ |
|||
│ │ │ │ │ │ |
|||
│ │ ┌─────────────┐│ │ ┌─────────────┐│ │ |
|||
│ │ │ Use ││ │ │ Don't ││ │ |
|||
│ │ │ Plugin ││ │ │ Use ││ │ |
|||
│ │ │ ││ │ │ Plugin ││ │ |
|||
│ │ │ ✅ Native ││ │ │ ││ │ |
|||
│ │ │ mobile ││ │ │ ❌ Web-only ││ │ |
|||
│ │ │ features ││ │ │ PWA ││ │ |
|||
│ │ │ ✅ Background││ │ │ ❌ No native││ │ |
|||
│ │ │ notifications││ │ │ features ││ │ |
|||
│ │ │ ✅ Platform ││ │ │ ❌ Use ││ │ |
|||
│ │ │ specific ││ │ │ existing ││ │ |
|||
│ │ │ optimizations││ │ │ code ││ │ |
|||
│ │ └─────────────┘│ │ └─────────────┘│ │ |
|||
│ └─────────────────┘ └─────────────────┘ │ |
|||
│ │ |
|||
└─────────────────────────────────────────────────────────────────────────────────┘ |
|||
``` |
|||
|
|||
## Detailed Decision Matrix |
|||
|
|||
| Scenario | Use Plugin? | Why? | What to Do | |
|||
|----------|-------------|------|------------| |
|||
| **Web-only PWA** | ❌ **NO** | Plugin requires Capacitor for native mobile features | Continue using your existing `loadNewStarredProjectChanges()` code | |
|||
| **Capacitor + Android** | ✅ **YES** | Plugin provides WorkManager, AlarmManager, notification channels | Integrate plugin with platform detection | |
|||
| **Capacitor + iOS** | ✅ **YES** | Plugin provides BGTaskScheduler, UNUserNotificationCenter | Integrate plugin with platform detection | |
|||
| **Capacitor + Electron** | ✅ **YES** | Plugin provides desktop notifications and background tasks | Integrate plugin with Electron-specific configuration | |
|||
| **Capacitor + Web** | ❌ **NO** | Web browser doesn't support native mobile features | Use existing web code, plugin won't work | |
|||
|
|||
## Code Examples |
|||
|
|||
### Scenario 1: Web-Only PWA (No Plugin) |
|||
|
|||
```typescript |
|||
// Your existing TimeSafari PWA code - NO CHANGES NEEDED |
|||
class TimeSafariHomeView { |
|||
private async loadNewStarredProjectChanges() { |
|||
if (this.activeDid && this.starredPlanHandleIds.length > 0) { |
|||
try { |
|||
const starredProjectChanges = await getStarredProjectsWithChanges( |
|||
this.axios, |
|||
this.apiServer, |
|||
this.activeDid, |
|||
this.starredPlanHandleIds, |
|||
this.lastAckedStarredPlanChangesJwtId, |
|||
); |
|||
this.numNewStarredProjectChanges = starredProjectChanges.data.length; |
|||
this.newStarredProjectChangesHitLimit = starredProjectChanges.hitLimit; |
|||
} catch (error) { |
|||
console.warn("[HomeView] Failed to load starred project changes:", error); |
|||
this.numNewStarredProjectChanges = 0; |
|||
this.newStarredProjectChangesHitLimit = false; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
**What happens:** |
|||
- Your existing code works perfectly in web browsers |
|||
- No plugin integration needed |
|||
- No native mobile features (background notifications, etc.) |
|||
- Limited to web browser capabilities |
|||
|
|||
### Scenario 2: Capacitor-Based PWA (Use Plugin) |
|||
|
|||
```typescript |
|||
// Your TimeSafari PWA with Capacitor - USE PLUGIN |
|||
import { Capacitor } from '@capacitor/core'; |
|||
import { DailyNotification } from '@timesafari/daily-notification-plugin'; |
|||
|
|||
class TimeSafariHomeView { |
|||
async initialize() { |
|||
// Check if running in Capacitor |
|||
if (Capacitor.isNativePlatform()) { |
|||
// Initialize plugin for native mobile features |
|||
await this.setupDailyNotification(); |
|||
} else { |
|||
// Use existing web-only code |
|||
console.log('Running in web browser - using existing code'); |
|||
} |
|||
} |
|||
|
|||
async setupDailyNotification() { |
|||
// Only runs on native platforms (Android, iOS) |
|||
await DailyNotification.configure({ |
|||
timesafariConfig: { |
|||
activeDid: this.activeDid, |
|||
endpoints: { |
|||
projectsLastUpdated: `${this.apiServer}/api/v2/report/plansLastUpdatedBetween` |
|||
}, |
|||
starredProjectsConfig: { |
|||
enabled: true, |
|||
starredPlanHandleIds: this.starredPlanHandleIds, |
|||
lastAckedJwtId: this.lastAckedStarredPlanChangesJwtId, |
|||
fetchInterval: '0 8 * * *' |
|||
} |
|||
}, |
|||
networkConfig: { |
|||
httpClient: this.axios, |
|||
baseURL: this.apiServer, |
|||
timeout: 30000 |
|||
} |
|||
}); |
|||
} |
|||
|
|||
async loadNewStarredProjectChanges() { |
|||
if (Capacitor.isNativePlatform()) { |
|||
// Use plugin-enhanced method on native platforms |
|||
await this.loadNewStarredProjectChangesNative(); |
|||
} else { |
|||
// Use existing web method in browser |
|||
await this.loadNewStarredProjectChangesWeb(); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
**What happens:** |
|||
- Plugin provides native mobile features (background notifications, etc.) |
|||
- Enhanced error handling and performance optimization |
|||
- Platform-specific optimizations (Android WorkManager, iOS BGTaskScheduler) |
|||
- Graceful fallback to web code when not on native platform |
|||
|
|||
## How to Check if You Need the Plugin |
|||
|
|||
### 1. Check if Capacitor is Installed |
|||
|
|||
```bash |
|||
# Check if Capacitor is in your package.json |
|||
npm list @capacitor/core |
|||
|
|||
# Or check your package.json file |
|||
cat package.json | grep capacitor |
|||
``` |
|||
|
|||
### 2. Check if Capacitor is Configured |
|||
|
|||
```bash |
|||
# Check if capacitor.config.ts exists |
|||
ls capacitor.config.ts |
|||
|
|||
# Or check if capacitor.config.js exists |
|||
ls capacitor.config.js |
|||
``` |
|||
|
|||
### 3. Check if Native Platforms are Added |
|||
|
|||
```bash |
|||
# Check if Android platform is added |
|||
ls android/ |
|||
|
|||
# Check if iOS platform is added |
|||
ls ios/ |
|||
``` |
|||
|
|||
### 4. Check in Your Code |
|||
|
|||
```typescript |
|||
// Add this to your TimeSafari PWA to check |
|||
import { Capacitor } from '@capacitor/core'; |
|||
|
|||
console.log('Platform:', Capacitor.getPlatform()); |
|||
console.log('Is Native:', Capacitor.isNativePlatform()); |
|||
console.log('Is Web:', Capacitor.getPlatform() === 'web'); |
|||
``` |
|||
|
|||
## Integration Checklist |
|||
|
|||
### ✅ If You Have Capacitor: |
|||
|
|||
- [ ] Install the DailyNotification plugin |
|||
- [ ] Add platform detection to your code |
|||
- [ ] Configure the plugin for your TimeSafari data |
|||
- [ ] Test on Android device/emulator |
|||
- [ ] Test on iOS device/simulator |
|||
- [ ] Test web fallback in browser |
|||
- [ ] Implement graceful degradation |
|||
|
|||
### ❌ If You Don't Have Capacitor: |
|||
|
|||
- [ ] Continue using your existing code |
|||
- [ ] No plugin integration needed |
|||
- [ ] No changes required |
|||
- [ ] Your existing `loadNewStarredProjectChanges()` works perfectly |
|||
|
|||
## Common Mistakes |
|||
|
|||
### ❌ Mistake 1: Installing Plugin Without Capacitor |
|||
|
|||
```typescript |
|||
// DON'T DO THIS - Plugin won't work in web browsers |
|||
import { DailyNotification } from '@timesafari/daily-notification-plugin'; |
|||
|
|||
// This will fail in web browsers |
|||
await DailyNotification.configure({...}); |
|||
``` |
|||
|
|||
### ❌ Mistake 2: Not Using Platform Detection |
|||
|
|||
```typescript |
|||
// DON'T DO THIS - Always tries to use plugin |
|||
await DailyNotification.configure({...}); // Fails in web browsers |
|||
``` |
|||
|
|||
### ✅ Correct Approach: Platform Detection |
|||
|
|||
```typescript |
|||
// DO THIS - Only use plugin on native platforms |
|||
import { Capacitor } from '@capacitor/core'; |
|||
|
|||
if (Capacitor.isNativePlatform()) { |
|||
await DailyNotification.configure({...}); |
|||
} else { |
|||
// Use existing web code |
|||
} |
|||
``` |
|||
|
|||
## Summary |
|||
|
|||
**The DailyNotification plugin is ONLY needed when your TimeSafari PWA uses Capacitor for mobile app development.** |
|||
|
|||
- **Web-only PWA**: Continue using your existing code, no plugin needed |
|||
- **Capacitor-based PWA**: Use the plugin with platform detection for native mobile features |
|||
- **Always implement graceful degradation** to fall back to web code when needed |
|||
|
|||
--- |
|||
|
|||
**Quick Answer**: If your TimeSafari PWA doesn't use Capacitor, you don't need the DailyNotification plugin. Your existing `loadNewStarredProjectChanges()` code works perfectly for web-only PWAs. |
@ -1,372 +0,0 @@ |
|||
# TimeSafari Daily Notification Plugin - Request Configuration Quick Reference |
|||
|
|||
**Author**: Matthew Raymer |
|||
**Version**: 1.0.0 |
|||
**Created**: 2025-10-08 06:24:57 UTC |
|||
|
|||
## Quick Setup for TimeSafari PWA |
|||
|
|||
### Basic Configuration |
|||
```typescript |
|||
import { DailyNotification } from '@timesafari/daily-notification-plugin'; |
|||
|
|||
await DailyNotification.configure({ |
|||
// Required: Active DID for TimeSafari integration |
|||
timesafariConfig: { |
|||
activeDid: userDid |
|||
}, |
|||
|
|||
// Basic network settings |
|||
networkConfig: { |
|||
timeout: 30000, |
|||
retryAttempts: 3, |
|||
maxConcurrent: 5 |
|||
} |
|||
}); |
|||
``` |
|||
|
|||
### Complete TimeSafari Configuration |
|||
```typescript |
|||
await DailyNotification.configure({ |
|||
// TimeSafari integration |
|||
timesafariConfig: { |
|||
activeDid: userDid, |
|||
endpoints: { |
|||
offersToPerson: 'https://endorser.ch/api/v1/offers/person', |
|||
offersToPlans: 'https://endorser.ch/api/v1/offers/plans' |
|||
}, |
|||
syncConfig: { |
|||
enableParallel: true, |
|||
maxConcurrent: 3, |
|||
batchSize: 10 |
|||
} |
|||
}, |
|||
|
|||
// Network configuration |
|||
networkConfig: { |
|||
timeout: 30000, |
|||
retryAttempts: 3, |
|||
retryDelay: 1000, |
|||
maxConcurrent: 5, |
|||
userAgent: 'TimeSafari-PWA/1.0.0' |
|||
}, |
|||
|
|||
// Authentication |
|||
authentication: { |
|||
jwt: { |
|||
secret: process.env.JWT_SECRET, |
|||
algorithm: 'HS256', |
|||
expirationMinutes: 60 |
|||
} |
|||
}, |
|||
|
|||
// Observability |
|||
logging: { |
|||
level: 'INFO', |
|||
enableRequestLogging: true, |
|||
redactSensitiveData: true |
|||
} |
|||
}); |
|||
``` |
|||
|
|||
## Platform-Specific Quick Configs |
|||
|
|||
### Android |
|||
```typescript |
|||
await DailyNotification.configure({ |
|||
platform: 'android', |
|||
androidConfig: { |
|||
workManagerConstraints: { |
|||
requiresNetworkType: 'CONNECTED', |
|||
requiresBatteryNotLow: false |
|||
}, |
|||
requestConfig: { |
|||
connectTimeout: 30000, |
|||
readTimeout: 30000 |
|||
} |
|||
} |
|||
}); |
|||
``` |
|||
|
|||
### iOS |
|||
```typescript |
|||
await DailyNotification.configure({ |
|||
platform: 'ios', |
|||
iosConfig: { |
|||
backgroundTasks: { |
|||
'com.timesafari.daily-notification-fetch': { |
|||
requiresNetworkConnectivity: true |
|||
} |
|||
}, |
|||
urlSessionConfig: { |
|||
timeoutIntervalForRequest: 30, |
|||
allowsCellularAccess: true |
|||
} |
|||
} |
|||
}); |
|||
``` |
|||
|
|||
### Electron |
|||
```typescript |
|||
await DailyNotification.configure({ |
|||
platform: 'electron', |
|||
electronConfig: { |
|||
sessionConfig: { |
|||
userAgent: 'TimeSafari-Desktop/1.0.0', |
|||
cacheEnabled: true |
|||
}, |
|||
requestConfig: { |
|||
timeout: 30000, |
|||
maxRedirects: 5 |
|||
} |
|||
} |
|||
}); |
|||
``` |
|||
|
|||
## Common Request Patterns |
|||
|
|||
### 1. Content Fetching |
|||
```typescript |
|||
await DailyNotification.configure({ |
|||
contentFetch: { |
|||
enabled: true, |
|||
schedule: '0 8 * * *', // Daily at 8 AM |
|||
url: 'https://endorser.ch/api/v1/community/updates', |
|||
headers: { |
|||
'Authorization': `Bearer ${authToken}`, |
|||
'X-User-DID': userDid |
|||
}, |
|||
timeout: 15000, |
|||
retryAttempts: 2 |
|||
} |
|||
}); |
|||
``` |
|||
|
|||
### 2. Callback Configuration |
|||
```typescript |
|||
await DailyNotification.configure({ |
|||
callbacks: { |
|||
apiService: 'https://api.timesafari.com/notifications/callback', |
|||
database: 'local://notification-storage', |
|||
reporting: 'https://analytics.timesafari.com/events', |
|||
onSuccess: async (data) => { |
|||
// Handle successful fetch |
|||
console.log('Content fetched:', data); |
|||
}, |
|||
onError: async (error) => { |
|||
// Handle error |
|||
console.error('Fetch error:', error); |
|||
} |
|||
} |
|||
}); |
|||
``` |
|||
|
|||
### 3. Circuit Breaker |
|||
```typescript |
|||
await DailyNotification.configure({ |
|||
circuitBreaker: { |
|||
failureThreshold: 5, |
|||
recoveryTimeout: 30000, |
|||
monitoringPeriod: 60000 |
|||
} |
|||
}); |
|||
``` |
|||
|
|||
### 4. Rate Limiting |
|||
```typescript |
|||
await DailyNotification.configure({ |
|||
rateLimiting: { |
|||
maxRequestsPerMinute: 30, |
|||
maxRequestsPerHour: 1000, |
|||
burstLimit: 10, |
|||
backoffStrategy: 'exponential' |
|||
} |
|||
}); |
|||
``` |
|||
|
|||
## Security Quick Configs |
|||
|
|||
### Certificate Pinning |
|||
```typescript |
|||
await DailyNotification.configure({ |
|||
security: { |
|||
certificatePinning: { |
|||
enabled: true, |
|||
pins: [ |
|||
{ |
|||
hostname: 'endorser.ch', |
|||
pins: ['sha256/YOUR_PIN_HERE'] |
|||
} |
|||
] |
|||
} |
|||
} |
|||
}); |
|||
``` |
|||
|
|||
### Data Protection |
|||
```typescript |
|||
await DailyNotification.configure({ |
|||
dataProtection: { |
|||
encryption: { |
|||
enabled: true, |
|||
algorithm: 'AES-256-GCM' |
|||
}, |
|||
redaction: { |
|||
enabled: true, |
|||
patterns: ['password', 'token', 'secret'] |
|||
} |
|||
} |
|||
}); |
|||
``` |
|||
|
|||
## Debugging Configs |
|||
|
|||
### Enable Debug Logging |
|||
```typescript |
|||
await DailyNotification.configure({ |
|||
logging: { |
|||
level: 'DEBUG', |
|||
enableRequestLogging: true, |
|||
enableResponseLogging: true, |
|||
enableErrorLogging: true |
|||
} |
|||
}); |
|||
``` |
|||
|
|||
### Disable Security for Development |
|||
```typescript |
|||
await DailyNotification.configure({ |
|||
security: { |
|||
certificatePinning: { |
|||
enabled: false // Only for development |
|||
} |
|||
} |
|||
}); |
|||
``` |
|||
|
|||
## Error Handling Quick Configs |
|||
|
|||
### Retry Configuration |
|||
```typescript |
|||
await DailyNotification.configure({ |
|||
retry: { |
|||
strategy: 'exponential', |
|||
initialDelay: 1000, |
|||
maxDelay: 30000, |
|||
multiplier: 2, |
|||
maxRetries: { |
|||
contentFetch: 3, |
|||
notificationDelivery: 2 |
|||
} |
|||
} |
|||
}); |
|||
``` |
|||
|
|||
### Fallback Configuration |
|||
```typescript |
|||
await DailyNotification.configure({ |
|||
fallback: { |
|||
contentFallbacks: [ |
|||
{ |
|||
condition: 'network_unavailable', |
|||
action: 'use_cached_content', |
|||
maxAge: 3600 |
|||
} |
|||
] |
|||
} |
|||
}); |
|||
``` |
|||
|
|||
## Vue.js Integration Example |
|||
|
|||
```typescript |
|||
// In your Vue component |
|||
import { defineComponent } from 'vue'; |
|||
import { DailyNotification } from '@timesafari/daily-notification-plugin'; |
|||
|
|||
export default defineComponent({ |
|||
name: 'NotificationSettings', |
|||
|
|||
async mounted() { |
|||
await DailyNotification.configure({ |
|||
timesafariConfig: { |
|||
activeDid: this.userDid |
|||
}, |
|||
contentFetch: { |
|||
enabled: true, |
|||
schedule: '0 9 * * *', |
|||
callbacks: { |
|||
onSuccess: this.handleSuccess, |
|||
onError: this.handleError |
|||
} |
|||
} |
|||
}); |
|||
}, |
|||
|
|||
methods: { |
|||
handleSuccess(data: Record<string, unknown>) { |
|||
this.notificationData = data; |
|||
}, |
|||
|
|||
handleError(error: Error) { |
|||
this.showError(error.message); |
|||
} |
|||
} |
|||
}); |
|||
``` |
|||
|
|||
## Environment Variables |
|||
|
|||
### Required Environment Variables |
|||
```bash |
|||
# TimeSafari configuration |
|||
JWT_SECRET=your_jwt_secret_here |
|||
API_KEY=your_api_key_here |
|||
|
|||
# Optional configuration |
|||
OAUTH_CLIENT_ID=your_oauth_client_id |
|||
OAUTH_CLIENT_SECRET=your_oauth_client_secret |
|||
``` |
|||
|
|||
### Development Environment |
|||
```bash |
|||
# Development settings |
|||
NODE_ENV=development |
|||
LOG_LEVEL=DEBUG |
|||
CERTIFICATE_PINNING=false |
|||
``` |
|||
|
|||
## Common Issues and Solutions |
|||
|
|||
### Issue: Network Timeout |
|||
**Solution**: Increase timeout |
|||
```typescript |
|||
networkConfig: { |
|||
timeout: 60000 // 60 seconds |
|||
} |
|||
``` |
|||
|
|||
### Issue: Authentication Errors |
|||
**Solution**: Check JWT configuration |
|||
```typescript |
|||
authentication: { |
|||
jwt: { |
|||
secret: process.env.JWT_SECRET, |
|||
algorithm: 'HS256' |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### Issue: Certificate Errors |
|||
**Solution**: Disable pinning for development |
|||
```typescript |
|||
security: { |
|||
certificatePinning: { |
|||
enabled: false |
|||
} |
|||
} |
|||
``` |
|||
|
|||
--- |
|||
|
|||
**For detailed configuration options, see**: `docs/host-request-configuration.md` |
@ -1,490 +0,0 @@ |
|||
# TimeSafari PWA - DailyNotification Plugin Integration Guide |
|||
|
|||
**Author**: Matthew Raymer |
|||
**Version**: 1.0.0 |
|||
**Created**: 2025-10-08 06:24:57 UTC |
|||
|
|||
## Overview |
|||
|
|||
This guide shows how to integrate the DailyNotification plugin with the existing TimeSafari PWA architecture, specifically the `CapacitorPlatformService` and `PlatformServiceMixin` patterns. The integration maintains the same interfaces and patterns while adding enhanced functionality. |
|||
|
|||
## TimeSafari PWA Architecture Analysis |
|||
|
|||
### Existing Architecture Components |
|||
|
|||
#### 1. CapacitorPlatformService |
|||
- **Location**: `src/services/platforms/CapacitorPlatformService.ts` |
|||
- **Purpose**: Provides native mobile functionality through Capacitor plugins |
|||
- **Features**: File system, camera, SQLite database operations, platform detection |
|||
- **Key Methods**: `dbQuery`, `dbExec`, `takePicture`, `writeFile`, `getCapabilities` |
|||
|
|||
#### 2. PlatformServiceFactory |
|||
- **Location**: `src/services/PlatformServiceFactory.ts` |
|||
- **Purpose**: Factory pattern for creating platform-specific service implementations |
|||
- **Pattern**: Singleton pattern with environment-based platform detection |
|||
- **Platforms**: `capacitor`, `electron`, `web` (default) |
|||
|
|||
#### 3. PlatformServiceMixin |
|||
- **Location**: `src/utils/PlatformServiceMixin.ts` |
|||
- **Purpose**: Vue.js mixin providing cached platform service access and utility methods |
|||
- **Features**: Database operations, settings management, contact management, logging |
|||
- **Key Methods**: `$db`, `$exec`, `$settings`, `$contacts`, `$saveSettings` |
|||
|
|||
### Existing TimeSafari Patterns |
|||
|
|||
#### 1. Settings Management |
|||
```typescript |
|||
// Existing TimeSafari settings pattern |
|||
const settings = await this.$settings(); |
|||
const activeDid = settings.activeDid; |
|||
const starredPlanHandleIds = settings.starredPlanHandleIds; |
|||
const apiServer = settings.apiServer; |
|||
``` |
|||
|
|||
#### 2. Database Operations |
|||
```typescript |
|||
// Existing TimeSafari database pattern |
|||
const result = await this.$dbQuery("SELECT * FROM settings WHERE id = 1"); |
|||
const contacts = await this.$contacts(); |
|||
await this.$saveSettings({ starredPlanHandleIds: newIds }); |
|||
``` |
|||
|
|||
#### 3. Platform Detection |
|||
```typescript |
|||
// Existing TimeSafari platform detection |
|||
if (this.isCapacitor) { |
|||
// Native mobile functionality |
|||
} else { |
|||
// Web browser functionality |
|||
} |
|||
``` |
|||
|
|||
## DailyNotification Plugin Integration |
|||
|
|||
### Integration Strategy |
|||
|
|||
The DailyNotification plugin integrates with the existing TimeSafari PWA architecture by: |
|||
|
|||
1. **Extending CapacitorPlatformService**: Adding DailyNotification functionality to the existing platform service |
|||
2. **Using PlatformServiceMixin**: Leveraging existing database and settings patterns |
|||
3. **Maintaining Interfaces**: Keeping the same method signatures and behavior |
|||
4. **Platform Detection**: Only initializing on Capacitor platforms |
|||
|
|||
### Enhanced CapacitorPlatformService |
|||
|
|||
```typescript |
|||
export class EnhancedCapacitorPlatformService { |
|||
private platformService = PlatformServiceFactory.getInstance(); |
|||
private dailyNotificationService: DailyNotification | null = null; |
|||
private integrationService: TimeSafariIntegrationService | null = null; |
|||
|
|||
async initializeDailyNotification(): Promise<void> { |
|||
// Get existing TimeSafari settings |
|||
const settings = await this.getTimeSafariSettings(); |
|||
|
|||
// Configure plugin with TimeSafari data |
|||
await DailyNotification.configure({ |
|||
timesafariConfig: { |
|||
activeDid: settings.activeDid, |
|||
endpoints: { |
|||
projectsLastUpdated: `${settings.apiServer}/api/v2/report/plansLastUpdatedBetween` |
|||
}, |
|||
starredProjectsConfig: { |
|||
enabled: true, |
|||
starredPlanHandleIds: settings.starredPlanHandleIds, |
|||
lastAckedJwtId: settings.lastAckedStarredPlanChangesJwtId, |
|||
fetchInterval: '0 8 * * *' |
|||
} |
|||
}, |
|||
networkConfig: { |
|||
baseURL: settings.apiServer, |
|||
timeout: 30000 |
|||
} |
|||
}); |
|||
} |
|||
|
|||
async loadNewStarredProjectChanges(): Promise<StarredProjectsResponse> { |
|||
// Enhanced version of existing TimeSafari method |
|||
const settings = await this.getTimeSafariSettings(); |
|||
|
|||
if (!settings.activeDid || !settings.starredPlanHandleIds?.length) { |
|||
return { data: [], hitLimit: false }; |
|||
} |
|||
|
|||
// Use plugin's enhanced fetching |
|||
return await this.integrationService!.getStarredProjectsWithChanges( |
|||
settings.activeDid, |
|||
settings.starredPlanHandleIds, |
|||
settings.lastAckedStarredPlanChangesJwtId |
|||
); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### Vue.js Component Integration |
|||
|
|||
```typescript |
|||
export const TimeSafariDailyNotificationMixin = { |
|||
data() { |
|||
return { |
|||
// Existing TimeSafari data |
|||
activeDid: '', |
|||
starredPlanHandleIds: [] as string[], |
|||
lastAckedStarredPlanChangesJwtId: '', |
|||
numNewStarredProjectChanges: 0, |
|||
newStarredProjectChangesHitLimit: false, |
|||
|
|||
// Plugin integration |
|||
enhancedPlatformService: null as EnhancedCapacitorPlatformService | null |
|||
}; |
|||
}, |
|||
|
|||
async mounted() { |
|||
// Initialize DailyNotification when component mounts (only on Capacitor) |
|||
if (this.isCapacitor) { |
|||
await this.initializeDailyNotification(); |
|||
} |
|||
}, |
|||
|
|||
methods: { |
|||
async initializeDailyNotification(): Promise<void> { |
|||
this.enhancedPlatformService = new EnhancedCapacitorPlatformService(); |
|||
await this.enhancedPlatformService.initializeDailyNotification(); |
|||
}, |
|||
|
|||
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(); |
|||
} |
|||
} |
|||
} |
|||
}; |
|||
``` |
|||
|
|||
## Integration Steps |
|||
|
|||
### Step 1: Install the Plugin |
|||
|
|||
```bash |
|||
# In TimeSafari PWA project |
|||
npm install ssh://git@173.199.124.46:222/trent_larson/daily-notification-plugin.git |
|||
``` |
|||
|
|||
### Step 2: Create Enhanced Platform Service |
|||
|
|||
Create `src/services/platforms/EnhancedCapacitorPlatformService.ts`: |
|||
|
|||
```typescript |
|||
import { DailyNotification } from '@timesafari/daily-notification-plugin'; |
|||
import { TimeSafariIntegrationService } from '@timesafari/daily-notification-plugin'; |
|||
import { PlatformServiceFactory } from '@/services/PlatformServiceFactory'; |
|||
|
|||
export class EnhancedCapacitorPlatformService { |
|||
// Implementation from the example above |
|||
} |
|||
``` |
|||
|
|||
### Step 3: Extend PlatformServiceMixin |
|||
|
|||
Add to `src/utils/PlatformServiceMixin.ts`: |
|||
|
|||
```typescript |
|||
export const PlatformServiceMixin = { |
|||
// ... existing mixin code ... |
|||
|
|||
methods: { |
|||
// ... existing methods ... |
|||
|
|||
/** |
|||
* Initialize DailyNotification plugin (Capacitor only) |
|||
*/ |
|||
async $initializeDailyNotification(): Promise<void> { |
|||
if (this.isCapacitor) { |
|||
const enhancedService = new EnhancedCapacitorPlatformService(); |
|||
await enhancedService.initializeDailyNotification(); |
|||
this._enhancedPlatformService = enhancedService; |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* Enhanced loadNewStarredProjectChanges method |
|||
*/ |
|||
async $loadNewStarredProjectChanges(): Promise<StarredProjectsResponse> { |
|||
if (this.isCapacitor && this._enhancedPlatformService) { |
|||
return await this._enhancedPlatformService.loadNewStarredProjectChanges(); |
|||
} else { |
|||
// Fall back to existing web method |
|||
return await this.$loadNewStarredProjectChangesWeb(); |
|||
} |
|||
} |
|||
} |
|||
}; |
|||
``` |
|||
|
|||
### Step 4: Update Vue Components |
|||
|
|||
In your existing TimeSafari Vue components: |
|||
|
|||
```typescript |
|||
export default defineComponent({ |
|||
name: 'TimeSafariHomeView', |
|||
|
|||
mixins: [PlatformServiceMixin], |
|||
|
|||
async mounted() { |
|||
// Initialize DailyNotification (only on Capacitor) |
|||
if (this.isCapacitor) { |
|||
await this.$initializeDailyNotification(); |
|||
} |
|||
}, |
|||
|
|||
methods: { |
|||
async loadNewStarredProjectChanges() { |
|||
// Use enhanced method |
|||
const result = await this.$loadNewStarredProjectChanges(); |
|||
this.numNewStarredProjectChanges = result.data.length; |
|||
this.newStarredProjectChangesHitLimit = result.hitLimit; |
|||
} |
|||
} |
|||
}); |
|||
``` |
|||
|
|||
## Configuration Mapping |
|||
|
|||
### TimeSafari Settings → Plugin Configuration |
|||
|
|||
| TimeSafari Setting | Plugin Configuration | Purpose | |
|||
|-------------------|---------------------|---------| |
|||
| `settings.activeDid` | `timesafariConfig.activeDid` | User authentication | |
|||
| `settings.apiServer` | `networkConfig.baseURL` | API endpoint | |
|||
| `settings.starredPlanHandleIds` | `starredProjectsConfig.starredPlanHandleIds` | Project IDs to fetch | |
|||
| `settings.lastAckedStarredPlanChangesJwtId` | `starredProjectsConfig.lastAckedJwtId` | Pagination token | |
|||
|
|||
### Existing Methods → Enhanced Methods |
|||
|
|||
| Existing Method | Enhanced Method | Enhancement | |
|||
|----------------|----------------|-------------| |
|||
| `loadNewStarredProjectChanges()` | `$loadNewStarredProjectChanges()` | Background fetching, structured logging | |
|||
| `getStarredProjectsWithChanges()` | `integrationService.getStarredProjectsWithChanges()` | Retry logic, circuit breaker | |
|||
| Settings storage | Plugin storage adapter | Tiered storage, TTL management | |
|||
|
|||
## Platform-Specific Behavior |
|||
|
|||
### Capacitor (Android/iOS) |
|||
- **Plugin Enabled**: Full DailyNotification functionality |
|||
- **Background Fetching**: WorkManager (Android), BGTaskScheduler (iOS) |
|||
- **Notifications**: Native notification channels and categories |
|||
- **Storage**: SQLite with plugin's tiered storage system |
|||
|
|||
### Web Browser |
|||
- **Plugin Disabled**: Uses existing TimeSafari web code |
|||
- **No Background Fetching**: Limited to browser capabilities |
|||
- **No Native Notifications**: Uses web notifications API |
|||
- **Storage**: Existing TimeSafari storage patterns |
|||
|
|||
### Electron |
|||
- **Plugin Enabled**: Desktop-specific functionality |
|||
- **Background Tasks**: Electron main process |
|||
- **Desktop Notifications**: Native desktop notifications |
|||
- **Storage**: SQLite with plugin's tiered storage system |
|||
|
|||
## Benefits of Integration |
|||
|
|||
### 1. Same Interface, Enhanced Functionality |
|||
- Existing `loadNewStarredProjectChanges()` method works exactly the same |
|||
- Enhanced with background fetching, structured logging, and error handling |
|||
- No changes required to existing UI code |
|||
|
|||
### 2. Leverages Existing TimeSafari Patterns |
|||
- Uses existing `PlatformServiceMixin` for database operations |
|||
- Integrates with existing settings management |
|||
- Maintains existing platform detection patterns |
|||
|
|||
### 3. Platform-Optimized Performance |
|||
- **Capacitor**: Native mobile optimizations with WorkManager/BGTaskScheduler |
|||
- **Web**: Falls back to existing web code seamlessly |
|||
- **Electron**: Desktop-specific optimizations |
|||
|
|||
### 4. Enhanced Observability |
|||
- Structured logging with event IDs |
|||
- Performance metrics and monitoring |
|||
- Error tracking and analysis |
|||
- Health checks and status monitoring |
|||
|
|||
## Migration Strategy |
|||
|
|||
### Phase 1: Parallel Implementation |
|||
1. **Keep existing code unchanged** |
|||
2. **Add enhanced platform service alongside existing code** |
|||
3. **Test both implementations in parallel** |
|||
4. **Compare results to ensure compatibility** |
|||
|
|||
### Phase 2: Gradual Migration |
|||
1. **Replace individual methods one by one** |
|||
2. **Use enhanced error handling and logging** |
|||
3. **Maintain existing UI and user experience** |
|||
4. **Add plugin-specific features gradually** |
|||
|
|||
### Phase 3: Full Integration |
|||
1. **Replace all TimeSafari request patterns with plugin** |
|||
2. **Remove duplicate code** |
|||
3. **Leverage plugin's advanced features** |
|||
4. **Optimize performance with plugin's caching and batching** |
|||
|
|||
## Testing Strategy |
|||
|
|||
### 1. Platform-Specific Testing |
|||
```typescript |
|||
// Test on different platforms |
|||
const testPlatforms = async () => { |
|||
if (Capacitor.getPlatform() === 'android') { |
|||
// Test Android-specific functionality |
|||
} else if (Capacitor.getPlatform() === 'ios') { |
|||
// Test iOS-specific functionality |
|||
} else { |
|||
// Test web fallback |
|||
} |
|||
}; |
|||
``` |
|||
|
|||
### 2. Parallel Testing |
|||
```typescript |
|||
// Test both implementations |
|||
const testBothImplementations = async () => { |
|||
// Existing TimeSafari implementation |
|||
const existingResult = await this.loadNewStarredProjectChangesOriginal(); |
|||
|
|||
// Plugin-enhanced implementation |
|||
const pluginResult = await this.$loadNewStarredProjectChanges(); |
|||
|
|||
// Compare results |
|||
assert.deepEqual(existingResult, pluginResult); |
|||
}; |
|||
``` |
|||
|
|||
### 3. Performance Testing |
|||
```typescript |
|||
// Compare performance |
|||
const performanceTest = async () => { |
|||
const start = Date.now(); |
|||
await this.$loadNewStarredProjectChanges(); |
|||
const pluginTime = Date.now() - start; |
|||
|
|||
console.log('Plugin performance:', pluginTime); |
|||
}; |
|||
``` |
|||
|
|||
## Common Integration Patterns |
|||
|
|||
### 1. Settings Integration |
|||
```typescript |
|||
// Get TimeSafari settings |
|||
const settings = await this.$settings(); |
|||
|
|||
// Configure plugin with settings |
|||
await DailyNotification.configure({ |
|||
timesafariConfig: { |
|||
activeDid: settings.activeDid, |
|||
starredPlanHandleIds: settings.starredPlanHandleIds |
|||
} |
|||
}); |
|||
``` |
|||
|
|||
### 2. Database Integration |
|||
```typescript |
|||
// Store plugin results in TimeSafari database |
|||
await this.$dbExec( |
|||
"INSERT OR REPLACE INTO temp (id, data) VALUES (?, ?)", |
|||
['starred_projects_latest', JSON.stringify(result)] |
|||
); |
|||
``` |
|||
|
|||
### 3. Error Handling Integration |
|||
```typescript |
|||
// Use existing TimeSafari error handling patterns |
|||
try { |
|||
const result = await this.$loadNewStarredProjectChanges(); |
|||
// Handle success |
|||
} catch (error) { |
|||
// Use existing TimeSafari error logging |
|||
await this.$logError(`Failed to load starred projects: ${error.message}`); |
|||
} |
|||
``` |
|||
|
|||
## Troubleshooting |
|||
|
|||
### Common Issues |
|||
|
|||
#### 1. Plugin Not Initializing |
|||
```typescript |
|||
// Check platform detection |
|||
if (!this.isCapacitor) { |
|||
console.log('Plugin only works on Capacitor platforms'); |
|||
return; |
|||
} |
|||
``` |
|||
|
|||
#### 2. Settings Not Found |
|||
```typescript |
|||
// Ensure settings are loaded |
|||
const settings = await this.$settings(); |
|||
if (!settings.activeDid) { |
|||
console.log('No active DID found'); |
|||
return; |
|||
} |
|||
``` |
|||
|
|||
#### 3. Database Connection Issues |
|||
```typescript |
|||
// Check database connection |
|||
try { |
|||
await this.$dbQuery("SELECT 1"); |
|||
} catch (error) { |
|||
console.error('Database connection failed:', error); |
|||
} |
|||
``` |
|||
|
|||
### Debug Methods |
|||
|
|||
#### 1. Plugin Status |
|||
```typescript |
|||
const status = await this.getDailyNotificationStatus(); |
|||
console.log('Plugin status:', status); |
|||
``` |
|||
|
|||
#### 2. Settings Debug |
|||
```typescript |
|||
await this.$debugMergedSettings(this.activeDid); |
|||
``` |
|||
|
|||
#### 3. Performance Debug |
|||
```typescript |
|||
const metrics = await DailyNotification.getMetrics(); |
|||
console.log('Plugin metrics:', metrics); |
|||
``` |
|||
|
|||
## Conclusion |
|||
|
|||
The DailyNotification plugin integrates seamlessly with the existing TimeSafari PWA architecture by: |
|||
|
|||
- **Extending CapacitorPlatformService** with enhanced functionality |
|||
- **Using PlatformServiceMixin** for database and settings operations |
|||
- **Maintaining existing interfaces** and method signatures |
|||
- **Providing platform-specific optimizations** for Capacitor, Web, and Electron |
|||
- **Enhancing observability** with structured logging and metrics |
|||
|
|||
The integration maintains the same developer experience while adding powerful new features like background fetching, native notifications, and comprehensive monitoring. |
|||
|
|||
--- |
|||
|
|||
**Next Steps:** |
|||
1. Review the integration example and understand the architecture |
|||
2. Test the enhanced platform service with existing TimeSafari code |
|||
3. Gradually migrate individual methods to use the plugin |
|||
4. Leverage the plugin's advanced features for enhanced user experience |
@ -1,682 +0,0 @@ |
|||
# TimeSafari Daily Notification Plugin - Request Pattern Adoption Guide |
|||
|
|||
**Author**: Matthew Raymer |
|||
**Version**: 1.0.0 |
|||
**Created**: 2025-10-08 06:24:57 UTC |
|||
|
|||
## Overview |
|||
|
|||
This guide shows how to directly adopt the existing TimeSafari PWA request patterns (like `loadNewStarredProjectChanges()` and `getStarredProjectsWithChanges()`) into the Daily Notification Plugin configuration. The plugin is designed to work seamlessly with TimeSafari's existing axios-based request architecture. |
|||
|
|||
## Current TimeSafari PWA Pattern Analysis |
|||
|
|||
### Original TimeSafari PWA Code |
|||
```typescript |
|||
// TimeSafari PWA - HomeView.vue |
|||
private async loadNewStarredProjectChanges() { |
|||
if (this.activeDid && this.starredPlanHandleIds.length > 0) { |
|||
try { |
|||
const starredProjectChanges = await getStarredProjectsWithChanges( |
|||
this.axios, |
|||
this.apiServer, |
|||
this.activeDid, |
|||
this.starredPlanHandleIds, |
|||
this.lastAckedStarredPlanChangesJwtId, |
|||
); |
|||
this.numNewStarredProjectChanges = starredProjectChanges.data.length; |
|||
this.newStarredProjectChangesHitLimit = starredProjectChanges.hitLimit; |
|||
} catch (error) { |
|||
logger.warn("[HomeView] Failed to load starred project changes:", error); |
|||
this.numNewStarredProjectChanges = 0; |
|||
this.newStarredProjectChangesHitLimit = false; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// TimeSafari PWA - API function |
|||
export async function getStarredProjectsWithChanges( |
|||
axios: Axios, |
|||
apiServer: string, |
|||
activeDid: string, |
|||
starredPlanHandleIds: string[], |
|||
afterId?: string, |
|||
): Promise<{ data: Array<PlanSummaryAndPreviousClaim>; hitLimit: boolean }> { |
|||
const url = `${apiServer}/api/v2/report/plansLastUpdatedBetween`; |
|||
const headers = await getHeaders(activeDid); |
|||
|
|||
const requestBody = { |
|||
planIds: starredPlanHandleIds, |
|||
afterId: afterId, |
|||
}; |
|||
|
|||
const response = await axios.post(url, requestBody, { headers }); |
|||
return response.data; |
|||
} |
|||
``` |
|||
|
|||
## Plugin Configuration Adoption |
|||
|
|||
### 1. Direct Axios Integration Pattern |
|||
|
|||
The plugin can be configured to use the host's existing axios instance and request patterns: |
|||
|
|||
```typescript |
|||
// In TimeSafari PWA - Plugin Configuration |
|||
import { DailyNotification } from '@timesafari/daily-notification-plugin'; |
|||
import { TimeSafariIntegrationService } from '@timesafari/daily-notification-plugin'; |
|||
|
|||
// Configure plugin with existing TimeSafari patterns |
|||
await DailyNotification.configure({ |
|||
// TimeSafari-specific configuration |
|||
timesafariConfig: { |
|||
activeDid: this.activeDid, |
|||
|
|||
// Use existing TimeSafari API endpoints |
|||
endpoints: { |
|||
offersToPerson: `${this.apiServer}/api/v2/offers/person`, |
|||
offersToPlans: `${this.apiServer}/api/v2/offers/plans`, |
|||
projectsLastUpdated: `${this.apiServer}/api/v2/report/plansLastUpdatedBetween` |
|||
}, |
|||
|
|||
// Configure starred projects fetching |
|||
starredProjectsConfig: { |
|||
enabled: true, |
|||
starredPlanHandleIds: this.starredPlanHandleIds, |
|||
lastAckedJwtId: this.lastAckedStarredPlanChangesJwtId, |
|||
fetchInterval: '0 8 * * *', // Daily at 8 AM |
|||
maxResults: 50, |
|||
hitLimitHandling: 'warn' // 'warn' | 'error' | 'ignore' |
|||
}, |
|||
|
|||
// Sync configuration matching TimeSafari patterns |
|||
syncConfig: { |
|||
enableParallel: true, |
|||
maxConcurrent: 3, |
|||
batchSize: 10, |
|||
timeout: 30000, |
|||
retryAttempts: 3 |
|||
} |
|||
}, |
|||
|
|||
// Network configuration using existing patterns |
|||
networkConfig: { |
|||
// Use existing axios instance |
|||
httpClient: this.axios, |
|||
baseURL: this.apiServer, |
|||
timeout: 30000, |
|||
retryAttempts: 3, |
|||
retryDelay: 1000, |
|||
|
|||
// Headers matching TimeSafari pattern |
|||
defaultHeaders: { |
|||
'Content-Type': 'application/json', |
|||
'Accept': 'application/json' |
|||
} |
|||
}, |
|||
|
|||
// Content fetch configuration |
|||
contentFetch: { |
|||
enabled: true, |
|||
schedule: '0 8 * * *', // Daily at 8 AM |
|||
|
|||
// Use existing TimeSafari request pattern |
|||
requestConfig: { |
|||
method: 'POST', |
|||
url: '${apiServer}/api/v2/report/plansLastUpdatedBetween', |
|||
headers: { |
|||
'Authorization': 'Bearer ${jwt}', |
|||
'X-User-DID': '${activeDid}' |
|||
}, |
|||
body: { |
|||
planIds: '${starredPlanHandleIds}', |
|||
afterId: '${lastAckedJwtId}' |
|||
} |
|||
}, |
|||
|
|||
// Callbacks matching TimeSafari error handling |
|||
callbacks: { |
|||
onSuccess: async (data: { data: Array<PlanSummaryAndPreviousClaim>; hitLimit: boolean }) => { |
|||
// Handle successful fetch - same as TimeSafari PWA |
|||
this.numNewStarredProjectChanges = data.data.length; |
|||
this.newStarredProjectChangesHitLimit = data.hitLimit; |
|||
|
|||
// Update UI |
|||
this.updateStarredProjectsUI(data); |
|||
}, |
|||
|
|||
onError: async (error: Error) => { |
|||
// Handle error - same as TimeSafari PWA |
|||
logger.warn("[DailyNotification] Failed to load starred project changes:", error); |
|||
this.numNewStarredProjectChanges = 0; |
|||
this.newStarredProjectChangesHitLimit = false; |
|||
}, |
|||
|
|||
onComplete: async (result: ContentFetchResult) => { |
|||
// Handle completion |
|||
logger.info("[DailyNotification] Starred projects fetch completed", { |
|||
success: result.success, |
|||
dataCount: result.data?.data?.length || 0, |
|||
hitLimit: result.data?.hitLimit || false |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
}); |
|||
``` |
|||
|
|||
### 2. Enhanced TimeSafari Integration Service |
|||
|
|||
The plugin provides an enhanced integration service that mirrors TimeSafari's patterns: |
|||
|
|||
```typescript |
|||
// In TimeSafari PWA - Enhanced Integration |
|||
import { TimeSafariIntegrationService } from '@timesafari/daily-notification-plugin'; |
|||
|
|||
// Initialize with existing TimeSafari configuration |
|||
const integrationService = TimeSafariIntegrationService.getInstance(); |
|||
|
|||
await integrationService.initialize({ |
|||
activeDid: this.activeDid, |
|||
storageAdapter: this.timeSafariStorageAdapter, |
|||
endorserApiBaseUrl: this.apiServer, |
|||
|
|||
// Use existing TimeSafari request patterns |
|||
requestConfig: { |
|||
httpClient: this.axios, |
|||
baseURL: this.apiServer, |
|||
timeout: 30000, |
|||
retryAttempts: 3 |
|||
}, |
|||
|
|||
// Configure starred projects fetching |
|||
starredProjectsConfig: { |
|||
enabled: true, |
|||
starredPlanHandleIds: this.starredPlanHandleIds, |
|||
lastAckedJwtId: this.lastAckedStarredPlanChangesJwtId, |
|||
fetchInterval: '0 8 * * *', |
|||
maxResults: 50 |
|||
} |
|||
}); |
|||
|
|||
// Use the service to fetch starred projects (same pattern as TimeSafari PWA) |
|||
const fetchStarredProjects = async () => { |
|||
try { |
|||
const starredProjectChanges = await integrationService.getStarredProjectsWithChanges( |
|||
this.activeDid, |
|||
this.starredPlanHandleIds, |
|||
this.lastAckedStarredPlanChangesJwtId |
|||
); |
|||
|
|||
// Same handling as TimeSafari PWA |
|||
this.numNewStarredProjectChanges = starredProjectChanges.data.length; |
|||
this.newStarredProjectChangesHitLimit = starredProjectChanges.hitLimit; |
|||
|
|||
} catch (error) { |
|||
// Same error handling as TimeSafari PWA |
|||
logger.warn("[DailyNotification] Failed to load starred project changes:", error); |
|||
this.numNewStarredProjectChanges = 0; |
|||
this.newStarredProjectChangesHitLimit = false; |
|||
} |
|||
}; |
|||
``` |
|||
|
|||
### 3. Vue.js Component Integration |
|||
|
|||
Direct integration into existing TimeSafari Vue components: |
|||
|
|||
```typescript |
|||
// In TimeSafari PWA - HomeView.vue (enhanced) |
|||
import { defineComponent } from 'vue'; |
|||
import { DailyNotification } from '@timesafari/daily-notification-plugin'; |
|||
import { TimeSafariIntegrationService } from '@timesafari/daily-notification-plugin'; |
|||
|
|||
export default defineComponent({ |
|||
name: 'HomeView', |
|||
|
|||
data() { |
|||
return { |
|||
// Existing TimeSafari data |
|||
activeDid: '', |
|||
starredPlanHandleIds: [] as string[], |
|||
lastAckedStarredPlanChangesJwtId: '', |
|||
numNewStarredProjectChanges: 0, |
|||
newStarredProjectChangesHitLimit: false, |
|||
|
|||
// Plugin integration |
|||
dailyNotificationService: null as DailyNotificationService | null, |
|||
integrationService: null as TimeSafariIntegrationService | null |
|||
}; |
|||
}, |
|||
|
|||
async mounted() { |
|||
// Initialize plugin with existing TimeSafari configuration |
|||
await this.initializeDailyNotifications(); |
|||
}, |
|||
|
|||
methods: { |
|||
async initializeDailyNotifications() { |
|||
try { |
|||
// Configure plugin with existing TimeSafari patterns |
|||
await DailyNotification.configure({ |
|||
timesafariConfig: { |
|||
activeDid: this.activeDid, |
|||
endpoints: { |
|||
projectsLastUpdated: `${this.apiServer}/api/v2/report/plansLastUpdatedBetween` |
|||
}, |
|||
starredProjectsConfig: { |
|||
enabled: true, |
|||
starredPlanHandleIds: this.starredPlanHandleIds, |
|||
lastAckedJwtId: this.lastAckedStarredPlanChangesJwtId, |
|||
fetchInterval: '0 8 * * *' |
|||
} |
|||
}, |
|||
|
|||
networkConfig: { |
|||
httpClient: this.axios, |
|||
baseURL: this.apiServer, |
|||
timeout: 30000 |
|||
}, |
|||
|
|||
contentFetch: { |
|||
enabled: true, |
|||
schedule: '0 8 * * *', |
|||
callbacks: { |
|||
onSuccess: this.handleStarredProjectsSuccess, |
|||
onError: this.handleStarredProjectsError |
|||
} |
|||
} |
|||
}); |
|||
|
|||
// Initialize integration service |
|||
this.integrationService = TimeSafariIntegrationService.getInstance(); |
|||
await this.integrationService.initialize({ |
|||
activeDid: this.activeDid, |
|||
storageAdapter: this.timeSafariStorageAdapter, |
|||
endorserApiBaseUrl: this.apiServer |
|||
}); |
|||
|
|||
// Replace existing method with plugin-enhanced version |
|||
this.loadNewStarredProjectChanges = this.loadNewStarredProjectChangesEnhanced; |
|||
|
|||
} catch (error) { |
|||
logger.error("[HomeView] Failed to initialize daily notifications:", error); |
|||
} |
|||
}, |
|||
|
|||
// Enhanced version of existing method |
|||
async loadNewStarredProjectChangesEnhanced() { |
|||
if (this.activeDid && this.starredPlanHandleIds.length > 0) { |
|||
try { |
|||
// Use plugin's enhanced fetching with same interface |
|||
const starredProjectChanges = await this.integrationService!.getStarredProjectsWithChanges( |
|||
this.activeDid, |
|||
this.starredPlanHandleIds, |
|||
this.lastAckedStarredPlanChangesJwtId |
|||
); |
|||
|
|||
// Same handling as original TimeSafari PWA |
|||
this.numNewStarredProjectChanges = starredProjectChanges.data.length; |
|||
this.newStarredProjectChangesHitLimit = starredProjectChanges.hitLimit; |
|||
|
|||
} catch (error) { |
|||
// Same error handling as original TimeSafari PWA |
|||
logger.warn("[HomeView] Failed to load starred project changes:", error); |
|||
this.numNewStarredProjectChanges = 0; |
|||
this.newStarredProjectChangesHitLimit = false; |
|||
} |
|||
} else { |
|||
this.numNewStarredProjectChanges = 0; |
|||
this.newStarredProjectChangesHitLimit = false; |
|||
} |
|||
}, |
|||
|
|||
// Callback handlers matching TimeSafari patterns |
|||
async handleStarredProjectsSuccess(data: { data: Array<PlanSummaryAndPreviousClaim>; hitLimit: boolean }) { |
|||
// Same handling as TimeSafari PWA |
|||
this.numNewStarredProjectChanges = data.data.length; |
|||
this.newStarredProjectChangesHitLimit = data.hitLimit; |
|||
|
|||
// Update UI |
|||
this.updateStarredProjectsUI(data); |
|||
}, |
|||
|
|||
async handleStarredProjectsError(error: Error) { |
|||
// Same error handling as TimeSafari PWA |
|||
logger.warn("[HomeView] Failed to load starred project changes:", error); |
|||
this.numNewStarredProjectChanges = 0; |
|||
this.newStarredProjectChangesHitLimit = false; |
|||
}, |
|||
|
|||
// Existing TimeSafari methods (unchanged) |
|||
updateStarredProjectsUI(data: { data: Array<PlanSummaryAndPreviousClaim>; hitLimit: boolean }) { |
|||
// Existing TimeSafari UI update logic |
|||
this.$emit('starred-projects-updated', data); |
|||
} |
|||
} |
|||
}); |
|||
``` |
|||
|
|||
## Plugin Implementation Details |
|||
|
|||
### 1. Enhanced TimeSafari Integration Service |
|||
|
|||
The plugin provides an enhanced version of TimeSafari's request patterns: |
|||
|
|||
```typescript |
|||
// Plugin implementation - Enhanced TimeSafari Integration Service |
|||
export class TimeSafariIntegrationService { |
|||
private static instance: TimeSafariIntegrationService; |
|||
private httpClient: AxiosInstance | null = null; |
|||
private baseURL: string = ''; |
|||
private activeDid: string = ''; |
|||
|
|||
static getInstance(): TimeSafariIntegrationService { |
|||
if (!TimeSafariIntegrationService.instance) { |
|||
TimeSafariIntegrationService.instance = new TimeSafariIntegrationService(); |
|||
} |
|||
return TimeSafariIntegrationService.instance; |
|||
} |
|||
|
|||
async initialize(config: { |
|||
activeDid: string; |
|||
httpClient: AxiosInstance; |
|||
baseURL: string; |
|||
storageAdapter: TimeSafariStorageAdapter; |
|||
endorserApiBaseUrl: string; |
|||
}): Promise<void> { |
|||
this.activeDid = config.activeDid; |
|||
this.httpClient = config.httpClient; |
|||
this.baseURL = config.baseURL; |
|||
|
|||
// Initialize with existing TimeSafari patterns |
|||
await this.initializeStorage(config.storageAdapter); |
|||
await this.initializeAuthentication(config.endorserApiBaseUrl); |
|||
} |
|||
|
|||
// Enhanced version of TimeSafari's getStarredProjectsWithChanges |
|||
async getStarredProjectsWithChanges( |
|||
activeDid: string, |
|||
starredPlanHandleIds: string[], |
|||
afterId?: string |
|||
): Promise<{ data: Array<PlanSummaryAndPreviousClaim>; hitLimit: boolean }> { |
|||
if (!starredPlanHandleIds || starredPlanHandleIds.length === 0) { |
|||
return { data: [], hitLimit: false }; |
|||
} |
|||
|
|||
if (!afterId) { |
|||
return { data: [], hitLimit: false }; |
|||
} |
|||
|
|||
try { |
|||
// Use existing TimeSafari request pattern |
|||
const url = `${this.baseURL}/api/v2/report/plansLastUpdatedBetween`; |
|||
const headers = await this.getHeaders(activeDid); |
|||
|
|||
const requestBody = { |
|||
planIds: starredPlanHandleIds, |
|||
afterId: afterId, |
|||
}; |
|||
|
|||
// Use host's axios instance |
|||
const response = await this.httpClient!.post(url, requestBody, { headers }); |
|||
|
|||
// Log with structured logging |
|||
observability.logEvent('INFO', EVENT_CODES.FETCH_SUCCESS, 'Starred projects fetched successfully', { |
|||
activeDid, |
|||
planCount: starredPlanHandleIds.length, |
|||
resultCount: response.data.data.length, |
|||
hitLimit: response.data.hitLimit |
|||
}); |
|||
|
|||
return response.data; |
|||
|
|||
} catch (error) { |
|||
// Enhanced error handling with structured logging |
|||
observability.logEvent('ERROR', EVENT_CODES.FETCH_FAILURE, 'Failed to fetch starred projects', { |
|||
activeDid, |
|||
planCount: starredPlanHandleIds.length, |
|||
error: (error as Error).message |
|||
}); |
|||
|
|||
throw error; |
|||
} |
|||
} |
|||
|
|||
// Enhanced version of TimeSafari's getHeaders |
|||
private async getHeaders(activeDid: string): Promise<Record<string, string>> { |
|||
// Use existing TimeSafari authentication pattern |
|||
const jwt = await this.getJWTToken(activeDid); |
|||
|
|||
return { |
|||
'Authorization': `Bearer ${jwt}`, |
|||
'Content-Type': 'application/json', |
|||
'Accept': 'application/json', |
|||
'X-User-DID': activeDid |
|||
}; |
|||
} |
|||
|
|||
// Enhanced JWT token management |
|||
private async getJWTToken(activeDid: string): Promise<string> { |
|||
// Use existing TimeSafari JWT pattern |
|||
const cachedToken = await this.getCachedToken(activeDid); |
|||
if (cachedToken && !this.isTokenExpired(cachedToken)) { |
|||
return cachedToken; |
|||
} |
|||
|
|||
// Generate new token using existing TimeSafari pattern |
|||
const newToken = await this.generateJWTToken(activeDid); |
|||
await this.cacheToken(activeDid, newToken); |
|||
|
|||
return newToken; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
### 2. Plugin Configuration Extensions |
|||
|
|||
The plugin extends the configuration to support TimeSafari's existing patterns: |
|||
|
|||
```typescript |
|||
// Plugin configuration extensions |
|||
export interface TimeSafariConfig { |
|||
activeDid: string; |
|||
|
|||
// Existing TimeSafari endpoints |
|||
endpoints?: { |
|||
offersToPerson?: string; |
|||
offersToPlans?: string; |
|||
projectsLastUpdated?: string; |
|||
}; |
|||
|
|||
// Enhanced starred projects configuration |
|||
starredProjectsConfig?: { |
|||
enabled: boolean; |
|||
starredPlanHandleIds: string[]; |
|||
lastAckedJwtId: string; |
|||
fetchInterval: string; // Cron expression |
|||
maxResults?: number; |
|||
hitLimitHandling?: 'warn' | 'error' | 'ignore'; |
|||
}; |
|||
|
|||
// Existing TimeSafari sync configuration |
|||
syncConfig?: { |
|||
enableParallel?: boolean; |
|||
maxConcurrent?: number; |
|||
batchSize?: number; |
|||
timeout?: number; |
|||
retryAttempts?: number; |
|||
}; |
|||
|
|||
// Enhanced error policy |
|||
errorPolicy?: { |
|||
maxRetries?: number; |
|||
backoffMultiplier?: number; |
|||
activeDidChangeRetries?: number; |
|||
starredProjectsRetries?: number; // New |
|||
}; |
|||
} |
|||
|
|||
export interface NetworkConfig { |
|||
// Use existing TimeSafari axios instance |
|||
httpClient?: AxiosInstance; |
|||
baseURL?: string; |
|||
timeout?: number; |
|||
retryAttempts?: number; |
|||
retryDelay?: number; |
|||
|
|||
// Existing TimeSafari headers |
|||
defaultHeaders?: Record<string, string>; |
|||
|
|||
// Enhanced request configuration |
|||
requestConfig?: { |
|||
method: 'GET' | 'POST' | 'PUT' | 'DELETE'; |
|||
url: string; |
|||
headers?: Record<string, string>; |
|||
body?: Record<string, unknown>; |
|||
params?: Record<string, string>; |
|||
}; |
|||
} |
|||
``` |
|||
|
|||
## Migration Strategy |
|||
|
|||
### Phase 1: Parallel Implementation |
|||
1. **Keep existing TimeSafari PWA code unchanged** |
|||
2. **Add plugin configuration alongside existing code** |
|||
3. **Test plugin functionality in parallel** |
|||
4. **Compare results between existing and plugin implementations** |
|||
|
|||
### Phase 2: Gradual Migration |
|||
1. **Replace individual request methods one by one** |
|||
2. **Use plugin's enhanced error handling and logging** |
|||
3. **Maintain existing UI and user experience** |
|||
4. **Add plugin-specific features (background fetching, etc.)** |
|||
|
|||
### Phase 3: Full Integration |
|||
1. **Replace all TimeSafari request patterns with plugin** |
|||
2. **Remove duplicate code** |
|||
3. **Leverage plugin's advanced features** |
|||
4. **Optimize performance with plugin's caching and batching** |
|||
|
|||
## Benefits of Plugin Adoption |
|||
|
|||
### 1. Enhanced Error Handling |
|||
```typescript |
|||
// Existing TimeSafari PWA |
|||
catch (error) { |
|||
logger.warn("[HomeView] Failed to load starred project changes:", error); |
|||
this.numNewStarredProjectChanges = 0; |
|||
} |
|||
|
|||
// Plugin-enhanced version |
|||
catch (error) { |
|||
// Structured logging with event IDs |
|||
observability.logEvent('WARN', EVENT_CODES.FETCH_FAILURE, 'Failed to load starred project changes', { |
|||
eventId: this.generateEventId(), |
|||
activeDid: this.activeDid, |
|||
planCount: this.starredPlanHandleIds.length, |
|||
error: error.message, |
|||
retryCount: this.retryCount |
|||
}); |
|||
|
|||
// Enhanced fallback handling |
|||
await this.handleStarredProjectsFallback(error); |
|||
} |
|||
``` |
|||
|
|||
### 2. Background Fetching |
|||
```typescript |
|||
// Plugin provides background fetching |
|||
await DailyNotification.configure({ |
|||
contentFetch: { |
|||
enabled: true, |
|||
schedule: '0 8 * * *', // Daily at 8 AM |
|||
backgroundFetch: true, // Fetch in background |
|||
cachePolicy: { |
|||
maxAge: 3600, // 1 hour cache |
|||
staleWhileRevalidate: 1800 // 30 minutes stale |
|||
} |
|||
} |
|||
}); |
|||
``` |
|||
|
|||
### 3. Enhanced Observability |
|||
```typescript |
|||
// Plugin provides comprehensive metrics |
|||
const metrics = await DailyNotification.getMetrics(); |
|||
console.log('Starred Projects Metrics:', { |
|||
fetchSuccessRate: metrics.starredProjects.fetchSuccessRate, |
|||
averageResponseTime: metrics.starredProjects.averageResponseTime, |
|||
cacheHitRate: metrics.starredProjects.cacheHitRate, |
|||
errorRate: metrics.starredProjects.errorRate |
|||
}); |
|||
``` |
|||
|
|||
## Testing Strategy |
|||
|
|||
### 1. Parallel Testing |
|||
```typescript |
|||
// Test both implementations in parallel |
|||
const testStarredProjectsFetch = async () => { |
|||
// Existing TimeSafari PWA implementation |
|||
const existingResult = await getStarredProjectsWithChanges( |
|||
this.axios, |
|||
this.apiServer, |
|||
this.activeDid, |
|||
this.starredPlanHandleIds, |
|||
this.lastAckedStarredPlanChangesJwtId |
|||
); |
|||
|
|||
// Plugin implementation |
|||
const pluginResult = await this.integrationService.getStarredProjectsWithChanges( |
|||
this.activeDid, |
|||
this.starredPlanHandleIds, |
|||
this.lastAckedStarredPlanChangesJwtId |
|||
); |
|||
|
|||
// Compare results |
|||
assert.deepEqual(existingResult, pluginResult); |
|||
}; |
|||
``` |
|||
|
|||
### 2. Performance Testing |
|||
```typescript |
|||
// Compare performance |
|||
const performanceTest = async () => { |
|||
const start = Date.now(); |
|||
|
|||
// Existing implementation |
|||
await getStarredProjectsWithChanges(...); |
|||
const existingTime = Date.now() - start; |
|||
|
|||
const pluginStart = Date.now(); |
|||
// Plugin implementation |
|||
await this.integrationService.getStarredProjectsWithChanges(...); |
|||
const pluginTime = Date.now() - pluginStart; |
|||
|
|||
console.log('Performance Comparison:', { |
|||
existing: existingTime, |
|||
plugin: pluginTime, |
|||
improvement: ((existingTime - pluginTime) / existingTime * 100).toFixed(2) + '%' |
|||
}); |
|||
}; |
|||
``` |
|||
|
|||
## Conclusion |
|||
|
|||
The Daily Notification Plugin is designed to seamlessly adopt TimeSafari's existing request patterns while providing enhanced functionality: |
|||
|
|||
- **Same Interface**: Plugin methods match existing TimeSafari patterns |
|||
- **Enhanced Features**: Background fetching, structured logging, metrics |
|||
- **Gradual Migration**: Can be adopted incrementally |
|||
- **Backward Compatibility**: Existing code continues to work |
|||
- **Performance Improvements**: Caching, batching, and optimization |
|||
|
|||
The plugin transforms TimeSafari's existing `loadNewStarredProjectChanges()` pattern into a more robust, observable, and efficient system while maintaining the same developer experience and user interface. |
|||
|
|||
--- |
|||
|
|||
**Next Steps**: |
|||
1. Implement parallel testing with existing TimeSafari PWA code |
|||
2. Gradually migrate individual request methods |
|||
3. Leverage plugin's advanced features for enhanced user experience |
@ -1,804 +0,0 @@ |
|||
/** |
|||
* 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<PlanSummaryAndPreviousClaim>; |
|||
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<void> | null = null; |
|||
private operationQueue: Array<QueuedOperation> = []; |
|||
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<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; |
|||
|
|||
// 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<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); |
|||
} |
|||
} |
|||
|
|||
// =================================================
|
|||
// 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<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; |
|||
} |
|||
} |
|||
|
|||
// =================================================
|
|||
// 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<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; |
|||
} |
|||
} |
|||
|
|||
// =================================================
|
|||
// 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<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 }; |
|||
} |
|||
} |
|||
|
|||
// =================================================
|
|||
// 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<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 {}; |
|||
} |
|||
} |
|||
|
|||
// =================================================
|
|||
// 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<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 |
|||
}); |
|||
} |
|||
|
|||
// =================================================
|
|||
// 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<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; |
|||
|
|||
// 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<void> { |
|||
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<QueryExecResult> { |
|||
await this.waitForInitialization(); |
|||
return this.queueOperation<QueryExecResult>("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)
|
|||
} |
@ -1,664 +0,0 @@ |
|||
/** |
|||
* TimeSafari PWA - CapacitorPlatformService Extension Example |
|||
* |
|||
* This example shows how to extend the existing CapacitorPlatformService |
|||
* in the TimeSafari PWA to add DailyNotification plugin functionality. |
|||
* |
|||
* This represents the ACTUAL CHANGES needed to the existing TimeSafari PWA code. |
|||
* |
|||
* @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<PlanSummaryAndPreviousClaim>; |
|||
hitLimit: boolean; |
|||
} |
|||
|
|||
interface TimeSafariSettings { |
|||
accountDid?: string; |
|||
activeDid?: string; |
|||
apiServer?: string; |
|||
starredPlanHandleIds?: string[]; |
|||
lastAckedStarredPlanChangesJwtId?: string; |
|||
[key: string]: unknown; |
|||
} |
|||
|
|||
/** |
|||
* EXTENDED CapacitorPlatformService with DailyNotification functionality |
|||
* |
|||
* This shows the ACTUAL CHANGES needed to the existing TimeSafari PWA |
|||
* CapacitorPlatformService class to add DailyNotification plugin support. |
|||
*/ |
|||
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<void> | null = null; |
|||
private operationQueue: Array<QueuedOperation> = []; |
|||
private isProcessingQueue: boolean = false; |
|||
|
|||
// =================================================
|
|||
// NEW PROPERTIES FOR DAILYNOTIFICATION PLUGIN
|
|||
// =================================================
|
|||
|
|||
private dailyNotificationService: DailyNotification | null = null; |
|||
private integrationService: TimeSafariIntegrationService | null = null; |
|||
private dailyNotificationInitialized = false; |
|||
|
|||
// =================================================
|
|||
// EXISTING CONSTRUCTOR (unchanged)
|
|||
// =================================================
|
|||
|
|||
constructor() { |
|||
this.sqlite = new SQLiteConnection(CapacitorSQLite); |
|||
} |
|||
|
|||
// =================================================
|
|||
// 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<void> { |
|||
if (this.dailyNotificationInitialized) { |
|||
return; |
|||
} |
|||
|
|||
try { |
|||
logger.log("[CapacitorPlatformService] Initializing DailyNotification plugin..."); |
|||
|
|||
// 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: { |
|||
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.dailyNotificationInitialized = true; |
|||
logger.log("[CapacitorPlatformService] DailyNotification plugin initialized successfully"); |
|||
|
|||
} catch (error) { |
|||
logger.error("[CapacitorPlatformService] Failed to initialize DailyNotification plugin:", error); |
|||
throw error; |
|||
} |
|||
} |
|||
|
|||
// =================================================
|
|||
// 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. |
|||
* |
|||
* This is the ACTUAL METHOD that would replace the existing TimeSafari method. |
|||
*/ |
|||
async loadNewStarredProjectChanges(): Promise<StarredProjectsResponse> { |
|||
// Ensure DailyNotification is initialized
|
|||
if (!this.dailyNotificationInitialized) { |
|||
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)
|
|||
logger.log("[CapacitorPlatformService] 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
|
|||
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<TimeSafariSettings> { |
|||
try { |
|||
// Use existing TimeSafari settings retrieval pattern
|
|||
const result = await this.dbQuery( |
|||
"SELECT * FROM settings WHERE id = 1" |
|||
); |
|||
|
|||
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]; |
|||
} |
|||
}); |
|||
|
|||
// 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 |
|||
* |
|||
* This method handles successful starred projects fetches and stores |
|||
* the results in the existing TimeSafari database for UI access. |
|||
*/ |
|||
private async handleStarredProjectsSuccess(data: StarredProjectsResponse): Promise<void> { |
|||
// Enhanced logging (optional)
|
|||
logger.log("[CapacitorPlatformService] Starred projects success callback:", { |
|||
count: data.data.length, |
|||
hitLimit: data.hitLimit |
|||
}); |
|||
|
|||
// 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 |
|||
* |
|||
* This method handles starred projects fetch errors and stores |
|||
* the error information in the existing TimeSafari database for UI access. |
|||
*/ |
|||
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() })] |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Callback handler for starred projects fetch completion |
|||
* |
|||
* This method handles the completion of starred projects fetches |
|||
* and provides additional logging and cleanup. |
|||
*/ |
|||
private async handleStarredProjectsComplete(result: unknown): Promise<void> { |
|||
// Handle completion
|
|||
logger.log("[CapacitorPlatformService] Starred projects fetch completed:", result); |
|||
} |
|||
|
|||
// =================================================
|
|||
// NEW METHOD: Get DailyNotification Status
|
|||
// =================================================
|
|||
|
|||
/** |
|||
* Get DailyNotification plugin status for debugging |
|||
* |
|||
* This method provides status information about the DailyNotification |
|||
* plugin for debugging and monitoring purposes. |
|||
*/ |
|||
async getDailyNotificationStatus(): Promise<{ |
|||
initialized: boolean; |
|||
platform: string; |
|||
capabilities: PlatformCapabilities; |
|||
}> { |
|||
return { |
|||
initialized: this.dailyNotificationInitialized, |
|||
platform: Capacitor.getPlatform(), |
|||
capabilities: this.getCapabilities() |
|||
}; |
|||
} |
|||
|
|||
// =================================================
|
|||
// EXISTING METHODS (unchanged - showing key ones)
|
|||
// =================================================
|
|||
|
|||
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; |
|||
|
|||
// 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; |
|||
} |
|||
} |
|||
|
|||
private async _initialize(): Promise<void> { |
|||
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(); |
|||
|
|||
// Set journal mode to WAL for better performance
|
|||
// await this.db.execute("PRAGMA journal_mode=WAL;");
|
|||
|
|||
// 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<QueryExecResult> { |
|||
await this.waitForInitialization(); |
|||
return this.queueOperation<QueryExecResult>("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)
|
|||
} |
@ -1,594 +0,0 @@ |
|||
/** |
|||
* 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() |
|||
}; |
|||
} |
@ -1,403 +0,0 @@ |
|||
/** |
|||
* TimeSafari PWA Integration Example |
|||
* |
|||
* This example shows how to directly adopt the existing TimeSafari PWA |
|||
* request patterns (like loadNewStarredProjectChanges) into the Daily |
|||
* Notification Plugin configuration. |
|||
* |
|||
* @author Matthew Raymer |
|||
* @version 1.0.0 |
|||
*/ |
|||
|
|||
import { DailyNotification } from '@timesafari/daily-notification-plugin'; |
|||
import { TimeSafariIntegrationService } from '@timesafari/daily-notification-plugin'; |
|||
import { AxiosInstance } from 'axios'; |
|||
|
|||
// TimeSafari PWA existing interfaces (for reference)
|
|||
interface PlanSummaryAndPreviousClaim { |
|||
id: string; |
|||
title: string; |
|||
description: string; |
|||
lastUpdated: string; |
|||
previousClaim?: unknown; |
|||
} |
|||
|
|||
interface StarredProjectsResponse { |
|||
data: Array<PlanSummaryAndPreviousClaim>; |
|||
hitLimit: boolean; |
|||
} |
|||
|
|||
// TimeSafari PWA existing class (for reference)
|
|||
class TimeSafariHomeView { |
|||
// Existing TimeSafari PWA properties
|
|||
activeDid: string = ''; |
|||
starredPlanHandleIds: string[] = []; |
|||
lastAckedStarredPlanChangesJwtId: string = ''; |
|||
numNewStarredProjectChanges: number = 0; |
|||
newStarredProjectChangesHitLimit: boolean = false; |
|||
apiServer: string = 'https://endorser.ch'; |
|||
axios: AxiosInstance; |
|||
|
|||
// Plugin integration properties
|
|||
private dailyNotificationService: DailyNotification | null = null; |
|||
private integrationService: TimeSafariIntegrationService | null = null; |
|||
|
|||
constructor(axiosInstance: AxiosInstance) { |
|||
this.axios = axiosInstance; |
|||
} |
|||
|
|||
/** |
|||
* Initialize Daily Notification Plugin with existing TimeSafari patterns |
|||
*/ |
|||
async initializeDailyNotifications(): Promise<void> { |
|||
try { |
|||
// Configure plugin with existing TimeSafari configuration
|
|||
await DailyNotification.configure({ |
|||
// TimeSafari-specific configuration
|
|||
timesafariConfig: { |
|||
activeDid: this.activeDid, |
|||
|
|||
// Use existing TimeSafari API endpoints
|
|||
endpoints: { |
|||
offersToPerson: `${this.apiServer}/api/v2/offers/person`, |
|||
offersToPlans: `${this.apiServer}/api/v2/offers/plans`, |
|||
projectsLastUpdated: `${this.apiServer}/api/v2/report/plansLastUpdatedBetween` |
|||
}, |
|||
|
|||
// Configure starred projects fetching (matches existing TimeSafari pattern)
|
|||
starredProjectsConfig: { |
|||
enabled: true, |
|||
starredPlanHandleIds: this.starredPlanHandleIds, |
|||
lastAckedJwtId: this.lastAckedStarredPlanChangesJwtId, |
|||
fetchInterval: '0 8 * * *', // Daily at 8 AM
|
|||
maxResults: 50, |
|||
hitLimitHandling: 'warn' // 'warn' | 'error' | 'ignore'
|
|||
}, |
|||
|
|||
// Sync configuration matching TimeSafari patterns
|
|||
syncConfig: { |
|||
enableParallel: true, |
|||
maxConcurrent: 3, |
|||
batchSize: 10, |
|||
timeout: 30000, |
|||
retryAttempts: 3 |
|||
} |
|||
}, |
|||
|
|||
// Network configuration using existing TimeSafari patterns
|
|||
networkConfig: { |
|||
// Use existing axios instance
|
|||
httpClient: this.axios, |
|||
baseURL: this.apiServer, |
|||
timeout: 30000, |
|||
retryAttempts: 3, |
|||
retryDelay: 1000, |
|||
|
|||
// Headers matching TimeSafari pattern
|
|||
defaultHeaders: { |
|||
'Content-Type': 'application/json', |
|||
'Accept': 'application/json' |
|||
} |
|||
}, |
|||
|
|||
// Content fetch configuration
|
|||
contentFetch: { |
|||
enabled: true, |
|||
schedule: '0 8 * * *', // Daily at 8 AM
|
|||
|
|||
// Use existing TimeSafari request pattern
|
|||
requestConfig: { |
|||
method: 'POST', |
|||
url: `${this.apiServer}/api/v2/report/plansLastUpdatedBetween`, |
|||
headers: { |
|||
'Authorization': 'Bearer ${jwt}', |
|||
'X-User-DID': '${activeDid}' |
|||
}, |
|||
body: { |
|||
planIds: '${starredPlanHandleIds}', |
|||
afterId: '${lastAckedJwtId}' |
|||
} |
|||
}, |
|||
|
|||
// Callbacks matching 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: this.activeDid, |
|||
storageAdapter: this.getTimeSafariStorageAdapter(), |
|||
endorserApiBaseUrl: this.apiServer, |
|||
|
|||
// Use existing TimeSafari request patterns
|
|||
requestConfig: { |
|||
httpClient: this.axios, |
|||
baseURL: this.apiServer, |
|||
timeout: 30000, |
|||
retryAttempts: 3 |
|||
}, |
|||
|
|||
// Configure starred projects fetching
|
|||
starredProjectsConfig: { |
|||
enabled: true, |
|||
starredPlanHandleIds: this.starredPlanHandleIds, |
|||
lastAckedJwtId: this.lastAckedStarredPlanChangesJwtId, |
|||
fetchInterval: '0 8 * * *', |
|||
maxResults: 50 |
|||
} |
|||
}); |
|||
|
|||
console.log('Daily notifications initialized successfully with TimeSafari patterns'); |
|||
|
|||
} catch (error) { |
|||
console.error('Failed to initialize daily notifications:', error); |
|||
throw error; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 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<void> { |
|||
if (this.activeDid && this.starredPlanHandleIds.length > 0) { |
|||
try { |
|||
// Use plugin's enhanced fetching with same interface as TimeSafari PWA
|
|||
const starredProjectChanges = await this.integrationService!.getStarredProjectsWithChanges( |
|||
this.activeDid, |
|||
this.starredPlanHandleIds, |
|||
this.lastAckedStarredPlanChangesJwtId |
|||
); |
|||
|
|||
// Same handling as original TimeSafari PWA
|
|||
this.numNewStarredProjectChanges = starredProjectChanges.data.length; |
|||
this.newStarredProjectChangesHitLimit = starredProjectChanges.hitLimit; |
|||
|
|||
// Enhanced logging with structured observability
|
|||
console.log('Starred projects loaded successfully:', { |
|||
count: this.numNewStarredProjectChanges, |
|||
hitLimit: this.newStarredProjectChangesHitLimit, |
|||
planIds: this.starredPlanHandleIds.length |
|||
}); |
|||
|
|||
} catch (error) { |
|||
// Same error handling as original TimeSafari PWA
|
|||
console.warn('[HomeView] Failed to load starred project changes:', error); |
|||
this.numNewStarredProjectChanges = 0; |
|||
this.newStarredProjectChangesHitLimit = false; |
|||
} |
|||
} else { |
|||
this.numNewStarredProjectChanges = 0; |
|||
this.newStarredProjectChangesHitLimit = false; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Callback handlers matching TimeSafari patterns |
|||
*/ |
|||
async handleStarredProjectsSuccess(data: StarredProjectsResponse): Promise<void> { |
|||
// Same handling as TimeSafari PWA
|
|||
this.numNewStarredProjectChanges = data.data.length; |
|||
this.newStarredProjectChangesHitLimit = data.hitLimit; |
|||
|
|||
// Update UI (existing TimeSafari method)
|
|||
this.updateStarredProjectsUI(data); |
|||
|
|||
// Enhanced logging
|
|||
console.log('Starred projects success callback:', { |
|||
count: data.data.length, |
|||
hitLimit: data.hitLimit |
|||
}); |
|||
} |
|||
|
|||
async handleStarredProjectsError(error: Error): Promise<void> { |
|||
// Same error handling as TimeSafari PWA
|
|||
console.warn('[HomeView] Failed to load starred project changes:', error); |
|||
this.numNewStarredProjectChanges = 0; |
|||
this.newStarredProjectChangesHitLimit = false; |
|||
|
|||
// Enhanced error handling with structured logging
|
|||
console.error('Starred projects error callback:', { |
|||
error: error.message, |
|||
activeDid: this.activeDid, |
|||
planCount: this.starredPlanHandleIds.length |
|||
}); |
|||
} |
|||
|
|||
async handleStarredProjectsComplete(result: unknown): Promise<void> { |
|||
// Handle completion
|
|||
console.log('Starred projects fetch completed:', result); |
|||
} |
|||
|
|||
/** |
|||
* Existing TimeSafari methods (unchanged) |
|||
*/ |
|||
private updateStarredProjectsUI(data: StarredProjectsResponse): void { |
|||
// Existing TimeSafari UI update logic
|
|||
console.log('Updating UI with starred projects data:', data); |
|||
} |
|||
|
|||
private getTimeSafariStorageAdapter(): unknown { |
|||
// Return existing TimeSafari storage adapter
|
|||
return { |
|||
// TimeSafari storage adapter implementation
|
|||
}; |
|||
} |
|||
} |
|||
|
|||
// Usage example
|
|||
export async function initializeTimeSafariWithPlugin(axiosInstance: AxiosInstance): Promise<TimeSafariHomeView> { |
|||
const homeView = new TimeSafariHomeView(axiosInstance); |
|||
|
|||
// Set up TimeSafari data (existing pattern)
|
|||
homeView.activeDid = 'did:example:123'; |
|||
homeView.starredPlanHandleIds = ['plan1', 'plan2', 'plan3']; |
|||
homeView.lastAckedStarredPlanChangesJwtId = 'jwt123'; |
|||
|
|||
// Initialize plugin with TimeSafari patterns
|
|||
await homeView.initializeDailyNotifications(); |
|||
|
|||
// Test the enhanced method
|
|||
await homeView.loadNewStarredProjectChanges(); |
|||
|
|||
return homeView; |
|||
} |
|||
|
|||
// Comparison example: Original vs Plugin-enhanced
|
|||
export class TimeSafariComparisonExample { |
|||
private axios: AxiosInstance; |
|||
|
|||
constructor(axiosInstance: AxiosInstance) { |
|||
this.axios = axiosInstance; |
|||
} |
|||
|
|||
/** |
|||
* Original TimeSafari PWA method (for comparison) |
|||
*/ |
|||
async loadNewStarredProjectChangesOriginal( |
|||
activeDid: string, |
|||
starredPlanHandleIds: string[], |
|||
lastAckedJwtId: string |
|||
): Promise<StarredProjectsResponse> { |
|||
if (!starredPlanHandleIds || starredPlanHandleIds.length === 0) { |
|||
return { data: [], hitLimit: false }; |
|||
} |
|||
|
|||
if (!lastAckedJwtId) { |
|||
return { data: [], hitLimit: false }; |
|||
} |
|||
|
|||
const url = `https://endorser.ch/api/v2/report/plansLastUpdatedBetween`; |
|||
const headers = await this.getHeaders(activeDid); |
|||
|
|||
const requestBody = { |
|||
planIds: starredPlanHandleIds, |
|||
afterId: lastAckedJwtId, |
|||
}; |
|||
|
|||
const response = await this.axios.post(url, requestBody, { headers }); |
|||
return response.data; |
|||
} |
|||
|
|||
/** |
|||
* Plugin-enhanced version (same interface, enhanced functionality) |
|||
*/ |
|||
async loadNewStarredProjectChangesEnhanced( |
|||
activeDid: string, |
|||
starredPlanHandleIds: string[], |
|||
lastAckedJwtId: string |
|||
): Promise<StarredProjectsResponse> { |
|||
// Use plugin's enhanced integration service
|
|||
const integrationService = TimeSafariIntegrationService.getInstance(); |
|||
|
|||
return await integrationService.getStarredProjectsWithChanges( |
|||
activeDid, |
|||
starredPlanHandleIds, |
|||
lastAckedJwtId |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Test both implementations in parallel |
|||
*/ |
|||
async testBothImplementations( |
|||
activeDid: string, |
|||
starredPlanHandleIds: string[], |
|||
lastAckedJwtId: string |
|||
): Promise<void> { |
|||
console.log('Testing both implementations in parallel...'); |
|||
|
|||
const start = Date.now(); |
|||
|
|||
// Test original implementation
|
|||
const originalResult = await this.loadNewStarredProjectChangesOriginal( |
|||
activeDid, |
|||
starredPlanHandleIds, |
|||
lastAckedJwtId |
|||
); |
|||
const originalTime = Date.now() - start; |
|||
|
|||
const pluginStart = Date.now(); |
|||
|
|||
// Test plugin-enhanced implementation
|
|||
const pluginResult = await this.loadNewStarredProjectChangesEnhanced( |
|||
activeDid, |
|||
starredPlanHandleIds, |
|||
lastAckedJwtId |
|||
); |
|||
const pluginTime = Date.now() - pluginStart; |
|||
|
|||
// Compare results
|
|||
console.log('Comparison Results:', { |
|||
original: { |
|||
time: originalTime, |
|||
count: originalResult.data.length, |
|||
hitLimit: originalResult.hitLimit |
|||
}, |
|||
plugin: { |
|||
time: pluginTime, |
|||
count: pluginResult.data.length, |
|||
hitLimit: pluginResult.hitLimit |
|||
}, |
|||
performance: { |
|||
improvement: ((originalTime - pluginTime) / originalTime * 100).toFixed(2) + '%' |
|||
} |
|||
}); |
|||
|
|||
// Verify results match
|
|||
if (JSON.stringify(originalResult) === JSON.stringify(pluginResult)) { |
|||
console.log('✅ Results match perfectly!'); |
|||
} else { |
|||
console.log('❌ Results differ:', { |
|||
original: originalResult, |
|||
plugin: pluginResult |
|||
}); |
|||
} |
|||
} |
|||
|
|||
private async getHeaders(activeDid: string): Promise<Record<string, string>> { |
|||
// Existing TimeSafari header generation
|
|||
return { |
|||
'Authorization': `Bearer ${await this.getJWTToken(activeDid)}`, |
|||
'Content-Type': 'application/json', |
|||
'Accept': 'application/json', |
|||
'X-User-DID': activeDid |
|||
}; |
|||
} |
|||
|
|||
private async getJWTToken(activeDid: string): Promise<string> { |
|||
// Existing TimeSafari JWT token generation
|
|||
return 'jwt-token-placeholder'; |
|||
} |
|||
} |
|||
|
|||
// Export for use in TimeSafari PWA
|
|||
export { TimeSafariHomeView, TimeSafariComparisonExample }; |
Loading…
Reference in new issue