Browse Source
- Documented active_identity table structure and access methods - Analyzed TimeSafari's PlatformServiceMixin patterns - Created three integration options: 1. Host-managed activeDid (plugin receives from host) 2. Plugin-lookup activeDid (plugin queries active_identity table) 3. Hybrid approach (recommended combination) - Detailed service layer integration points - Cross-platform considerations for Android/Capacitor/Web/iOS/Electron - Security isolation recommendations for plugin/host database access This analysis provides clear guidance for hosting the plugin within TimeSafari applications.research/notification-plugin-enhancement
1 changed files with 343 additions and 0 deletions
@ -0,0 +1,343 @@ |
|||||
|
# TimeSafari Daily Notification Plugin Integration Analysis |
||||
|
|
||||
|
**Author**: Matthew Raymer |
||||
|
**Version**: 1.0.0 |
||||
|
**Created**: 2025-10-02 11:00:00 UTC |
||||
|
**Last Updated**: 2025-10-02 11:00:00 UTC |
||||
|
|
||||
|
## Overview |
||||
|
|
||||
|
This document analyzes how the Daily Notification Plugin would integrate with TimeSafari, focusing on `activeDid` access and plugin hosting architecture. |
||||
|
|
||||
|
## ActiveDid Management in TimeSafari |
||||
|
|
||||
|
### Database Schema: `active_identity` Table |
||||
|
|
||||
|
**Table Structure:** |
||||
|
```sql |
||||
|
CREATE TABLE active_identity ( |
||||
|
id INTEGER PRIMARY KEY DEFAULT 1, |
||||
|
activeDid TEXT NOT NULL, |
||||
|
lastUpdated DATETIME DEFAULT (datetime('now')) |
||||
|
); |
||||
|
``` |
||||
|
|
||||
|
**Key Characteristics:** |
||||
|
- Single row table with `id = 1` always |
||||
|
- `activeDid` contains the currently active user's DID |
||||
|
- `lastUpdated` tracks when the active identity was switched |
||||
|
- Single source of truth for current user identity |
||||
|
|
||||
|
### Access Methods |
||||
|
|
||||
|
**1. Platform Service Level:** |
||||
|
```typescript |
||||
|
// In CapacitorPlatformService, WebPlatformService, ElectronPlatformService |
||||
|
async updateActiveDid(did: string): Promise<void> { |
||||
|
await this.dbExec( |
||||
|
"INSERT OR REPLACE INTO active_identity (id, activeDid, lastUpdated) VALUES (1, ?, ?)", |
||||
|
[did, new Date().toISOString()] |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
async getActiveIdentity(): Promise<{ activeDid: string }> { |
||||
|
const result = await this.dbQuery( |
||||
|
"SELECT activeDid FROM active_identity WHERE id = 1" |
||||
|
); |
||||
|
return { |
||||
|
activeDid: (result?.values?.[0]?.[0] as string) || "" |
||||
|
}; |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
**2. Vue Mixin Level:** |
||||
|
```typescript |
||||
|
// In PlatformServiceMixin.ts |
||||
|
async $updateActiveDid(newDid: string | null): Promise<void> { |
||||
|
await this.$dbExec( |
||||
|
"UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1", |
||||
|
[newDid || ""] |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
async $getActiveIdentity(): Promise<{ activeDid: string }> { |
||||
|
const result = await this.$dbExec( |
||||
|
"SELECT activeDid FROM active_identity WHERE id = 1" |
||||
|
); |
||||
|
return { activeDid: result.activeDid || "" }; |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
**3. Component Level Usage:** |
||||
|
```typescript |
||||
|
// In Vue components |
||||
|
const activeIdentity = await this.$getActiveIdentity(); |
||||
|
const activeDid = activeIdentity.activeDid || ""; |
||||
|
``` |
||||
|
|
||||
|
## Plugin Integration Options |
||||
|
|
||||
|
### Option 1: Host Application Manages activeDid |
||||
|
|
||||
|
**Pros:** |
||||
|
- Simple integration |
||||
|
- Plugin doesn't need database access |
||||
|
- Clear separation of concerns |
||||
|
|
||||
|
**Implementation:** |
||||
|
```typescript |
||||
|
// In TimeSafari application |
||||
|
import { DailyNotification } from '@timesafari/daily-notification-plugin'; |
||||
|
|
||||
|
// On app initialization or user switch |
||||
|
async function initializeNotifications() { |
||||
|
const activeIdentity = await this.$getActiveIdentity(); |
||||
|
const activeDid = activeIdentity.activeDid; |
||||
|
|
||||
|
if (activeDid) { |
||||
|
await DailyNotification.setActiveDid(activeDid); |
||||
|
await DailyNotification.configure({ |
||||
|
apiServer: 'https://endorser.ch', |
||||
|
activeDid: activeDid, |
||||
|
// ... other config |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
**When to Use:** |
||||
|
- Host application already has database access |
||||
|
- Plugin should be stateless regarding identity |
||||
|
- Simple notification scheduling only |
||||
|
|
||||
|
### Option 2: Plugin Looks Up activeDid |
||||
|
|
||||
|
**Pros:** |
||||
|
- Plugin manages its own identity context |
||||
|
- Automatic activeDid synchronization |
||||
|
- Background tasks have direct access |
||||
|
|
||||
|
**Implementation:** |
||||
|
```typescript |
||||
|
// Plugin has database access via @capacitor-community/sqlite |
||||
|
interface EnhancedDailyNotificationPlugin { |
||||
|
async initializeFromTimeSafari(): Promise<void> { |
||||
|
// Plugin accesses TimeSafari's active_identity table |
||||
|
const result = await this.dbQuery( |
||||
|
"SELECT activeDid FROM active_identity WHERE id = 1" |
||||
|
); |
||||
|
const activeDid = result?.values?.[0]?.[0] as string; |
||||
|
|
||||
|
await this.setActiveDid(activeDid); |
||||
|
await this.scheduleBackgroundTasks(); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
**When to Use:** |
||||
|
- Plugin needs autonomous background operation |
||||
|
- Host application integration should be minimal |
||||
|
- Plugin operates independently of app lifecycle |
||||
|
|
||||
|
### Option 3: Hybrid Approach (Recommended) |
||||
|
|
||||
|
**Implementation:** |
||||
|
```typescript |
||||
|
// Host provides activeDid when app is active |
||||
|
// Plugin looks up activeDid for background operations |
||||
|
|
||||
|
interface HybridDailyNotificationPlugin { |
||||
|
// Host-initiated activeDid setting |
||||
|
async setActiveDidFromHost(activeDid: string): Promise<void>; |
||||
|
|
||||
|
// Plugin-initiated activeDid lookup for background |
||||
|
async refreshActiveDidFromDatabase(): Promise<string>; |
||||
|
|
||||
|
// Automatic mode that uses both approaches |
||||
|
async enableAutoActiveDidMode(): Promise<void>; |
||||
|
} |
||||
|
|
||||
|
// In TimeSafari app |
||||
|
async function initializeNotifications() { |
||||
|
const activeIdentity = await this.$getActiveIdentity(); |
||||
|
const activeDid = activeIdentity.activeDid; |
||||
|
|
||||
|
if (activeDid) { |
||||
|
await DailyNotification.setActiveDidFromHost(activeDid); |
||||
|
await DailyNotification.enableAutoActiveDidMode(); |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Recommended Plugin Hosting Architecture |
||||
|
|
||||
|
### Service Layer Integration |
||||
|
|
||||
|
**Location**: `src/services/DailyNotificationService.ts` |
||||
|
|
||||
|
```typescript |
||||
|
// src/services/DailyNotificationService.ts |
||||
|
import { DailyNotification } from '@timesafari/daily-notification-plugin'; |
||||
|
import { PlatformServiceFactory } from './PlatformServiceFactory'; |
||||
|
|
||||
|
export class DailyNotificationService { |
||||
|
private plugin: DailyNotification; |
||||
|
private platform = PlatformServiceFactory.getInstance(); |
||||
|
private currentActiveDid: string | null = null; |
||||
|
|
||||
|
async initialize(): Promise<void> { |
||||
|
// Initialize plugin with current active identity |
||||
|
const activeIdentity = await this.platform.getActiveIdentity(); |
||||
|
this.currentActiveDid = activeIdentity.activeDid; |
||||
|
|
||||
|
if (this.currentActiveDid) { |
||||
|
await this.plugin.setActiveDid(this.currentActiveDid); |
||||
|
await this.plugin.configure({ |
||||
|
apiServer: import.meta.env.VITE_ENDORSER_API_SERVER, |
||||
|
storageType: 'plugin-managed', |
||||
|
notifications: { |
||||
|
offers: true, |
||||
|
projects: true, |
||||
|
people: true, |
||||
|
items: true |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// Listen for identity changes |
||||
|
this.setupIdentityChangeListener(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private setupIdentityChangeListener(): void { |
||||
|
// Subscribe to identity changes in TimeSafari |
||||
|
document.addEventListener('activeDidChanged', (event: CustomEvent) => { |
||||
|
this.handleActiveDidChange(event.detail.activeDid); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
private async handleActiveDidChange(newActiveDid: string): Promise<void> { |
||||
|
if (newActiveDid !== this.currentActiveDid) { |
||||
|
this.currentActiveDid = newActiveDid; |
||||
|
await this.plugin.setActiveDid(newActiveDid); |
||||
|
logger.info(`[DailyNotificationService] ActiveDid updated to: ${newActiveDid}`); |
||||
|
|
||||
|
``` |
||||
|
|
||||
|
### PlatformServiceMixin Integration |
||||
|
|
||||
|
**Location**: `src/utils/PlatformServiceMixin.ts` |
||||
|
|
||||
|
```typescript |
||||
|
// Add to PlatformServiceMixin.ts |
||||
|
async $notifyDailyNotificationService(): Promise<void> { |
||||
|
// Trigger notification service update when activeDid changes |
||||
|
const event = new CustomEvent('activeDidChanged', { |
||||
|
detail: { activeDid: this.currentActiveDid } |
||||
|
}); |
||||
|
document.dispatchEvent(event); |
||||
|
} |
||||
|
|
||||
|
// Modify existing $updateActiveDid method |
||||
|
async $updateActiveDid(newDid: string | null): Promise<void> { |
||||
|
// ... existing logic ... |
||||
|
|
||||
|
// Notify DailyNotification service |
||||
|
await this.$notifyDailyNotificationService(); |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### App Initialization Integration |
||||
|
|
||||
|
**Location**: `src/main.ts` or App component |
||||
|
|
||||
|
```typescript |
||||
|
// src/main.ts |
||||
|
import { DailyNotificationService } from '@/services/DailyNotificationService'; |
||||
|
|
||||
|
async function initializeNotifications() { |
||||
|
try { |
||||
|
const notificationService = new DailyNotificationService(); |
||||
|
await notificationService.initialize(); |
||||
|
|
||||
|
logger.info('[App] Daily notification service initialized successfully'); |
||||
|
} catch (error) { |
||||
|
logger.error('[App] Failed to initialize notification service:', error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Call after platform service initialization |
||||
|
initializePlatform().then(async () => { |
||||
|
await initializeNotifications(); |
||||
|
// ... rest of app initialization |
||||
|
}); |
||||
|
``` |
||||
|
|
||||
|
## Cross-Platform Considerations |
||||
|
|
||||
|
### Android (Capacitor) |
||||
|
- **Database Access**: Plugin uses `@capacitor-community/sqlite` |
||||
|
- **identity Access**: Can read TimeSafari's active_identity table directly |
||||
|
- **Background Tasks**: WorkManager can query active_identity independently |
||||
|
|
||||
|
### Web (absurd-sql) |
||||
|
- **Database Access**: Plugin delegates to host application |
||||
|
- **identity Access**: Host provides activeDid, plugin doesn't access database |
||||
|
- **Background Tasks**: Service Worker coordination with host database |
||||
|
|
||||
|
### iOS (Capacitor) |
||||
|
- **Database Access**: Plugin uses CapacitorSQLite |
||||
|
- **identity Access**: Can read TimeSafari's active_identity table |
||||
|
- **Background Tasks**: BGTaskScheduler coordination |
||||
|
|
||||
|
### Electron |
||||
|
- **Database Access**: Plugin uses `@capacitor-community/sqlite` + filesystem |
||||
|
- **identity Access**: Direct file-based database access |
||||
|
- **Background Tasks**: Electron main process coordination |
||||
|
|
||||
|
## Security Considerations |
||||
|
|
||||
|
### activeDid Isolation |
||||
|
- Plugin should not modify active_identity table |
||||
|
- Plugin should only read current activeDid |
||||
|
- Identity changes should be initiated by TimeSafari host application |
||||
|
|
||||
|
### Token Management |
||||
|
- JWT tokens should use activeDid for both `iss` and `sub` |
||||
|
- Token generation should happen in plugin context |
||||
|
- Host application should not handle authentication tokens |
||||
|
|
||||
|
### Background Execution |
||||
|
- Background tasks should validate activeDid hasn't changed |
||||
|
- Implement cache invalidation when activeDid changes |
||||
|
- Handle edge cases where activeDid becomes empty/invalid |
||||
|
|
||||
|
## Implementation Priority |
||||
|
|
||||
|
### Phase 1: Basic Integration |
||||
|
1. Host-managed activeDid (Option 1) |
||||
|
2. Service layer integration |
||||
|
3. Basic notification scheduling |
||||
|
|
||||
|
### Phase 2: Enhanced Integration |
||||
|
1. Hybrid activeDid management (Option 3) |
||||
|
2. Background database access |
||||
|
3. Automatic identity synchronization |
||||
|
|
||||
|
### Phase 3: Advanced Features |
||||
|
1. Cross-platform optimization |
||||
|
2. Advanced background task coordination |
||||
|
3. Performance monitoring and analytics |
||||
|
|
||||
|
## Success Criteria |
||||
|
|
||||
|
- [ ] Plugin correctly receives activeDid from TimeSafari host |
||||
|
- [ ] Background notifications work across identity changes |
||||
|
- [] Cross-platform consistency maintained |
||||
|
- [ ] Minimal impact on TimeSafari performance |
||||
|
- [ ] Secure isolation between plugin and host database operations |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
**Status**: Architectural analysis complete - Ready for implementation planning |
||||
|
**Next Steps**: Choose integration approach and begin Phase 1 implementation |
||||
|
**Dependencies**: TimeSafari active_identity table access, PlatformServiceMixin integration |
Loading…
Reference in new issue