Browse Source
- Add comprehensive configuration system with timesafari-config.json - Create shared config-loader.ts with TypeScript interfaces and mock services - Update Android test app to use TimeSafari community notification patterns - Update iOS test app with rolling window and community features - Update Electron test app with desktop-specific TimeSafari integration - Enhance test API server to simulate Endorser.ch API endpoints - Add pagination support with afterId/beforeId parameters - Implement parallel API requests pattern for offers, projects, people, items - Add community analytics and notification bundle endpoints - Update all test app UIs for TimeSafari-specific functionality - Update README with comprehensive TimeSafari testing guide All test apps now demonstrate: - Real Endorser.ch API integration patterns - TimeSafari community-building features - Platform-specific optimizations (Android/iOS/Electron) - Comprehensive error handling and performance monitoring - Configuration-driven testing with type safetyresearch/notification-plugin-enhancement
11 changed files with 3431 additions and 388 deletions
File diff suppressed because it is too large
@ -0,0 +1,152 @@ |
|||
{ |
|||
"timesafari": { |
|||
"appId": "app.timesafari.test", |
|||
"appName": "TimeSafari Test", |
|||
"version": "1.0.0", |
|||
"description": "Test app for TimeSafari Daily Notification Plugin integration" |
|||
}, |
|||
"endorser": { |
|||
"baseUrl": "http://localhost:3001", |
|||
"apiVersion": "v2", |
|||
"endpoints": { |
|||
"offers": "/api/v2/report/offers", |
|||
"offersToPlans": "/api/v2/report/offersToPlansOwnedByMe", |
|||
"plansLastUpdated": "/api/v2/report/plansLastUpdatedBetween", |
|||
"notificationsBundle": "/api/v2/report/notifications/bundle" |
|||
}, |
|||
"authentication": { |
|||
"type": "Bearer", |
|||
"token": "test-jwt-token-12345", |
|||
"headers": { |
|||
"Authorization": "Bearer test-jwt-token-12345", |
|||
"Content-Type": "application/json", |
|||
"X-Privacy-Level": "user-controlled" |
|||
} |
|||
}, |
|||
"pagination": { |
|||
"defaultLimit": 50, |
|||
"maxLimit": 100, |
|||
"hitLimitThreshold": 50 |
|||
} |
|||
}, |
|||
"notificationTypes": { |
|||
"offers": { |
|||
"enabled": true, |
|||
"types": [ |
|||
"new_to_me", |
|||
"changed_to_me", |
|||
"new_to_projects", |
|||
"changed_to_projects", |
|||
"new_to_favorites", |
|||
"changed_to_favorites" |
|||
] |
|||
}, |
|||
"projects": { |
|||
"enabled": true, |
|||
"types": [ |
|||
"local_new", |
|||
"local_changed", |
|||
"content_interest_new", |
|||
"favorited_changed" |
|||
] |
|||
}, |
|||
"people": { |
|||
"enabled": true, |
|||
"types": [ |
|||
"local_new", |
|||
"local_changed", |
|||
"content_interest_new", |
|||
"favorited_changed", |
|||
"contacts_changed" |
|||
] |
|||
}, |
|||
"items": { |
|||
"enabled": true, |
|||
"types": [ |
|||
"local_new", |
|||
"local_changed", |
|||
"favorited_changed" |
|||
] |
|||
} |
|||
}, |
|||
"scheduling": { |
|||
"contentFetch": { |
|||
"schedule": "0 8 * * *", |
|||
"time": "08:00", |
|||
"description": "8 AM daily - fetch community updates" |
|||
}, |
|||
"userNotification": { |
|||
"schedule": "0 9 * * *", |
|||
"time": "09:00", |
|||
"description": "9 AM daily - notify users of community updates" |
|||
} |
|||
}, |
|||
"testData": { |
|||
"userDid": "did:example:testuser123", |
|||
"starredPlanIds": [ |
|||
"plan-community-garden", |
|||
"plan-local-food", |
|||
"plan-sustainability" |
|||
], |
|||
"lastKnownOfferId": "01HSE3R9MAC0FT3P3KZ382TWV7", |
|||
"lastKnownPlanId": "01HSE3R9MAC0FT3P3KZ382TWV8", |
|||
"mockOffers": [ |
|||
{ |
|||
"jwtId": "01HSE3R9MAC0FT3P3KZ382TWV7", |
|||
"handleId": "offer-web-dev-001", |
|||
"offeredByDid": "did:example:offerer123", |
|||
"recipientDid": "did:example:testuser123", |
|||
"objectDescription": "Web development services for community project", |
|||
"unit": "USD", |
|||
"amount": 1000, |
|||
"amountGiven": 500, |
|||
"amountGivenConfirmed": 250 |
|||
} |
|||
], |
|||
"mockProjects": [ |
|||
{ |
|||
"plan": { |
|||
"jwtId": "01HSE3R9MAC0FT3P3KZ382TWV8", |
|||
"handleId": "plan-community-garden", |
|||
"name": "Community Garden Project", |
|||
"description": "Building a community garden for local food production", |
|||
"issuerDid": "did:example:issuer123", |
|||
"agentDid": "did:example:agent123" |
|||
}, |
|||
"wrappedClaimBefore": null |
|||
} |
|||
] |
|||
}, |
|||
"callbacks": { |
|||
"offers": { |
|||
"enabled": true, |
|||
"localHandler": "handleOffersNotification" |
|||
}, |
|||
"projects": { |
|||
"enabled": true, |
|||
"localHandler": "handleProjectsNotification" |
|||
}, |
|||
"people": { |
|||
"enabled": true, |
|||
"localHandler": "handlePeopleNotification" |
|||
}, |
|||
"items": { |
|||
"enabled": true, |
|||
"localHandler": "handleItemsNotification" |
|||
}, |
|||
"communityAnalytics": { |
|||
"enabled": true, |
|||
"endpoint": "http://localhost:3001/api/analytics/community-events", |
|||
"headers": { |
|||
"Content-Type": "application/json", |
|||
"X-Privacy-Level": "aggregated" |
|||
} |
|||
} |
|||
}, |
|||
"observability": { |
|||
"enableLogging": true, |
|||
"logLevel": "debug", |
|||
"enableMetrics": true, |
|||
"enableHealthChecks": true |
|||
} |
|||
} |
@ -0,0 +1,522 @@ |
|||
/** |
|||
* Configuration loader for TimeSafari test apps |
|||
* |
|||
* Loads configuration from JSON files and provides typed access |
|||
* to TimeSafari-specific settings, Endorser.ch API endpoints, |
|||
* and test data. |
|||
* |
|||
* @author Matthew Raymer |
|||
* @version 1.0.0 |
|||
*/ |
|||
|
|||
export interface TimeSafariConfig { |
|||
timesafari: { |
|||
appId: string; |
|||
appName: string; |
|||
version: string; |
|||
description: string; |
|||
}; |
|||
endorser: { |
|||
baseUrl: string; |
|||
apiVersion: string; |
|||
endpoints: { |
|||
offers: string; |
|||
offersToPlans: string; |
|||
plansLastUpdated: string; |
|||
notificationsBundle: string; |
|||
}; |
|||
authentication: { |
|||
type: string; |
|||
token: string; |
|||
headers: Record<string, string>; |
|||
}; |
|||
pagination: { |
|||
defaultLimit: number; |
|||
maxLimit: number; |
|||
hitLimitThreshold: number; |
|||
}; |
|||
}; |
|||
notificationTypes: { |
|||
offers: { |
|||
enabled: boolean; |
|||
types: string[]; |
|||
}; |
|||
projects: { |
|||
enabled: boolean; |
|||
types: string[]; |
|||
}; |
|||
people: { |
|||
enabled: boolean; |
|||
types: string[]; |
|||
}; |
|||
items: { |
|||
enabled: boolean; |
|||
types: string[]; |
|||
}; |
|||
}; |
|||
scheduling: { |
|||
contentFetch: { |
|||
schedule: string; |
|||
time: string; |
|||
description: string; |
|||
}; |
|||
userNotification: { |
|||
schedule: string; |
|||
time: string; |
|||
description: string; |
|||
}; |
|||
}; |
|||
testData: { |
|||
userDid: string; |
|||
starredPlanIds: string[]; |
|||
lastKnownOfferId: string; |
|||
lastKnownPlanId: string; |
|||
mockOffers: any[]; |
|||
mockProjects: any[]; |
|||
}; |
|||
callbacks: { |
|||
offers: { |
|||
enabled: boolean; |
|||
localHandler: string; |
|||
}; |
|||
projects: { |
|||
enabled: boolean; |
|||
localHandler: string; |
|||
}; |
|||
people: { |
|||
enabled: boolean; |
|||
localHandler: string; |
|||
}; |
|||
items: { |
|||
enabled: boolean; |
|||
localHandler: string; |
|||
}; |
|||
communityAnalytics: { |
|||
enabled: boolean; |
|||
endpoint: string; |
|||
headers: Record<string, string>; |
|||
}; |
|||
}; |
|||
observability: { |
|||
enableLogging: boolean; |
|||
logLevel: string; |
|||
enableMetrics: boolean; |
|||
enableHealthChecks: boolean; |
|||
}; |
|||
} |
|||
|
|||
/** |
|||
* Configuration loader class |
|||
*/ |
|||
export class ConfigLoader { |
|||
private static instance: ConfigLoader; |
|||
private config: TimeSafariConfig | null = null; |
|||
|
|||
private constructor() {} |
|||
|
|||
/** |
|||
* Get singleton instance |
|||
*/ |
|||
public static getInstance(): ConfigLoader { |
|||
if (!ConfigLoader.instance) { |
|||
ConfigLoader.instance = new ConfigLoader(); |
|||
} |
|||
return ConfigLoader.instance; |
|||
} |
|||
|
|||
/** |
|||
* Load configuration from JSON file |
|||
*/ |
|||
public async loadConfig(): Promise<TimeSafariConfig> { |
|||
if (this.config) { |
|||
return this.config; |
|||
} |
|||
|
|||
try { |
|||
// In a real app, this would fetch from a config file
|
|||
// For test apps, we'll use a hardcoded config
|
|||
this.config = { |
|||
timesafari: { |
|||
appId: "app.timesafari.test", |
|||
appName: "TimeSafari Test", |
|||
version: "1.0.0", |
|||
description: "Test app for TimeSafari Daily Notification Plugin integration" |
|||
}, |
|||
endorser: { |
|||
baseUrl: "http://localhost:3001", |
|||
apiVersion: "v2", |
|||
endpoints: { |
|||
offers: "/api/v2/report/offers", |
|||
offersToPlans: "/api/v2/report/offersToPlansOwnedByMe", |
|||
plansLastUpdated: "/api/v2/report/plansLastUpdatedBetween", |
|||
notificationsBundle: "/api/v2/report/notifications/bundle" |
|||
}, |
|||
authentication: { |
|||
type: "Bearer", |
|||
token: "test-jwt-token-12345", |
|||
headers: { |
|||
"Authorization": "Bearer test-jwt-token-12345", |
|||
"Content-Type": "application/json", |
|||
"X-Privacy-Level": "user-controlled" |
|||
} |
|||
}, |
|||
pagination: { |
|||
defaultLimit: 50, |
|||
maxLimit: 100, |
|||
hitLimitThreshold: 50 |
|||
} |
|||
}, |
|||
notificationTypes: { |
|||
offers: { |
|||
enabled: true, |
|||
types: [ |
|||
"new_to_me", |
|||
"changed_to_me", |
|||
"new_to_projects", |
|||
"changed_to_projects", |
|||
"new_to_favorites", |
|||
"changed_to_favorites" |
|||
] |
|||
}, |
|||
projects: { |
|||
enabled: true, |
|||
types: [ |
|||
"local_new", |
|||
"local_changed", |
|||
"content_interest_new", |
|||
"favorited_changed" |
|||
] |
|||
}, |
|||
people: { |
|||
enabled: true, |
|||
types: [ |
|||
"local_new", |
|||
"local_changed", |
|||
"content_interest_new", |
|||
"favorited_changed", |
|||
"contacts_changed" |
|||
] |
|||
}, |
|||
items: { |
|||
enabled: true, |
|||
types: [ |
|||
"local_new", |
|||
"local_changed", |
|||
"favorited_changed" |
|||
] |
|||
} |
|||
}, |
|||
scheduling: { |
|||
contentFetch: { |
|||
schedule: "0 8 * * *", |
|||
time: "08:00", |
|||
description: "8 AM daily - fetch community updates" |
|||
}, |
|||
userNotification: { |
|||
schedule: "0 9 * * *", |
|||
time: "09:00", |
|||
description: "9 AM daily - notify users of community updates" |
|||
} |
|||
}, |
|||
testData: { |
|||
userDid: "did:example:testuser123", |
|||
starredPlanIds: [ |
|||
"plan-community-garden", |
|||
"plan-local-food", |
|||
"plan-sustainability" |
|||
], |
|||
lastKnownOfferId: "01HSE3R9MAC0FT3P3KZ382TWV7", |
|||
lastKnownPlanId: "01HSE3R9MAC0FT3P3KZ382TWV8", |
|||
mockOffers: [ |
|||
{ |
|||
jwtId: "01HSE3R9MAC0FT3P3KZ382TWV7", |
|||
handleId: "offer-web-dev-001", |
|||
offeredByDid: "did:example:offerer123", |
|||
recipientDid: "did:example:testuser123", |
|||
objectDescription: "Web development services for community project", |
|||
unit: "USD", |
|||
amount: 1000, |
|||
amountGiven: 500, |
|||
amountGivenConfirmed: 250 |
|||
} |
|||
], |
|||
mockProjects: [ |
|||
{ |
|||
plan: { |
|||
jwtId: "01HSE3R9MAC0FT3P3KZ382TWV8", |
|||
handleId: "plan-community-garden", |
|||
name: "Community Garden Project", |
|||
description: "Building a community garden for local food production", |
|||
issuerDid: "did:example:issuer123", |
|||
agentDid: "did:example:agent123" |
|||
}, |
|||
wrappedClaimBefore: null |
|||
} |
|||
] |
|||
}, |
|||
callbacks: { |
|||
offers: { |
|||
enabled: true, |
|||
localHandler: "handleOffersNotification" |
|||
}, |
|||
projects: { |
|||
enabled: true, |
|||
localHandler: "handleProjectsNotification" |
|||
}, |
|||
people: { |
|||
enabled: true, |
|||
localHandler: "handlePeopleNotification" |
|||
}, |
|||
items: { |
|||
enabled: true, |
|||
localHandler: "handleItemsNotification" |
|||
}, |
|||
communityAnalytics: { |
|||
enabled: true, |
|||
endpoint: "http://localhost:3001/api/analytics/community-events", |
|||
headers: { |
|||
"Content-Type": "application/json", |
|||
"X-Privacy-Level": "aggregated" |
|||
} |
|||
} |
|||
}, |
|||
observability: { |
|||
enableLogging: true, |
|||
logLevel: "debug", |
|||
enableMetrics: true, |
|||
enableHealthChecks: true |
|||
} |
|||
}; |
|||
|
|||
return this.config; |
|||
} catch (error) { |
|||
console.error('Failed to load configuration:', error); |
|||
throw error; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Get configuration |
|||
*/ |
|||
public getConfig(): TimeSafariConfig { |
|||
if (!this.config) { |
|||
throw new Error('Configuration not loaded. Call loadConfig() first.'); |
|||
} |
|||
return this.config; |
|||
} |
|||
|
|||
/** |
|||
* Get Endorser.ch API URL for a specific endpoint |
|||
*/ |
|||
public getEndorserUrl(endpoint: keyof TimeSafariConfig['endorser']['endpoints']): string { |
|||
const config = this.getConfig(); |
|||
return `${config.endorser.baseUrl}${config.endorser.endpoints[endpoint]}`; |
|||
} |
|||
|
|||
/** |
|||
* Get authentication headers |
|||
*/ |
|||
public getAuthHeaders(): Record<string, string> { |
|||
const config = this.getConfig(); |
|||
return config.endorser.authentication.headers; |
|||
} |
|||
|
|||
/** |
|||
* Get test data |
|||
*/ |
|||
public getTestData() { |
|||
const config = this.getConfig(); |
|||
return config.testData; |
|||
} |
|||
|
|||
/** |
|||
* Get notification types for a specific category |
|||
*/ |
|||
public getNotificationTypes(category: keyof TimeSafariConfig['notificationTypes']) { |
|||
const config = this.getConfig(); |
|||
return config.notificationTypes[category]; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Logger utility for test apps |
|||
*/ |
|||
export class TestLogger { |
|||
private logLevel: string; |
|||
|
|||
constructor(logLevel: string = 'debug') { |
|||
this.logLevel = logLevel; |
|||
} |
|||
|
|||
private shouldLog(level: string): boolean { |
|||
const levels = ['error', 'warn', 'info', 'debug']; |
|||
return levels.indexOf(level) <= levels.indexOf(this.logLevel); |
|||
} |
|||
|
|||
public debug(message: string, data?: any) { |
|||
if (this.shouldLog('debug')) { |
|||
console.log(`[DEBUG] ${message}`, data || ''); |
|||
} |
|||
} |
|||
|
|||
public info(message: string, data?: any) { |
|||
if (this.shouldLog('info')) { |
|||
console.log(`[INFO] ${message}`, data || ''); |
|||
} |
|||
} |
|||
|
|||
public warn(message: string, data?: any) { |
|||
if (this.shouldLog('warn')) { |
|||
console.warn(`[WARN] ${message}`, data || ''); |
|||
} |
|||
} |
|||
|
|||
public error(message: string, data?: any) { |
|||
if (this.shouldLog('error')) { |
|||
console.error(`[ERROR] ${message}`, data || ''); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Mock DailyNotificationService for test apps |
|||
*/ |
|||
export class MockDailyNotificationService { |
|||
private config: TimeSafariConfig; |
|||
private logger: TestLogger; |
|||
private isInitialized = false; |
|||
|
|||
constructor(config: TimeSafariConfig) { |
|||
this.config = config; |
|||
this.logger = new TestLogger(config.observability.logLevel); |
|||
} |
|||
|
|||
/** |
|||
* Initialize the service |
|||
*/ |
|||
public async initialize(): Promise<void> { |
|||
this.logger.info('Initializing Mock DailyNotificationService'); |
|||
this.isInitialized = true; |
|||
this.logger.info('Mock DailyNotificationService initialized successfully'); |
|||
} |
|||
|
|||
/** |
|||
* Schedule dual notification (content fetch + user notification) |
|||
*/ |
|||
public async scheduleDualNotification(config: any): Promise<void> { |
|||
if (!this.isInitialized) { |
|||
throw new Error('Service not initialized'); |
|||
} |
|||
|
|||
this.logger.info('Scheduling dual notification', config); |
|||
|
|||
// Simulate content fetch
|
|||
if (config.contentFetch?.enabled) { |
|||
await this.simulateContentFetch(config.contentFetch); |
|||
} |
|||
|
|||
// Simulate user notification
|
|||
if (config.userNotification?.enabled) { |
|||
await this.simulateUserNotification(config.userNotification); |
|||
} |
|||
|
|||
this.logger.info('Dual notification scheduled successfully'); |
|||
} |
|||
|
|||
/** |
|||
* Register callback |
|||
*/ |
|||
public async registerCallback(name: string, callback: Function): Promise<void> { |
|||
this.logger.info(`Registering callback: ${name}`); |
|||
// In a real implementation, this would register the callback
|
|||
this.logger.info(`Callback ${name} registered successfully`); |
|||
} |
|||
|
|||
/** |
|||
* Get dual schedule status |
|||
*/ |
|||
public async getDualScheduleStatus(): Promise<any> { |
|||
return { |
|||
contentFetch: { |
|||
enabled: true, |
|||
lastFetch: new Date().toISOString(), |
|||
nextFetch: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString() |
|||
}, |
|||
userNotification: { |
|||
enabled: true, |
|||
lastNotification: new Date().toISOString(), |
|||
nextNotification: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString() |
|||
} |
|||
}; |
|||
} |
|||
|
|||
/** |
|||
* Cancel all notifications |
|||
*/ |
|||
public async cancelAllNotifications(): Promise<void> { |
|||
this.logger.info('Cancelling all notifications'); |
|||
this.logger.info('All notifications cancelled successfully'); |
|||
} |
|||
|
|||
/** |
|||
* Simulate content fetch using Endorser.ch API patterns |
|||
*/ |
|||
private async simulateContentFetch(config: any): Promise<void> { |
|||
this.logger.info('Simulating content fetch from Endorser.ch API'); |
|||
|
|||
try { |
|||
// Simulate parallel API requests
|
|||
const testData = this.config.testData; |
|||
|
|||
// Mock offers to person
|
|||
const offersToPerson = { |
|||
data: testData.mockOffers, |
|||
hitLimit: false |
|||
}; |
|||
|
|||
// Mock offers to projects
|
|||
const offersToProjects = { |
|||
data: [], |
|||
hitLimit: false |
|||
}; |
|||
|
|||
// Mock starred project changes
|
|||
const starredChanges = { |
|||
data: testData.mockProjects, |
|||
hitLimit: false |
|||
}; |
|||
|
|||
this.logger.info('Content fetch simulation completed', { |
|||
offersToPerson: offersToPerson.data.length, |
|||
offersToProjects: offersToProjects.data.length, |
|||
starredChanges: starredChanges.data.length |
|||
}); |
|||
|
|||
// Call success callback if provided
|
|||
if (config.callbacks?.onSuccess) { |
|||
await config.callbacks.onSuccess({ |
|||
offersToPerson, |
|||
offersToProjects, |
|||
starredChanges |
|||
}); |
|||
} |
|||
} catch (error) { |
|||
this.logger.error('Content fetch simulation failed', error); |
|||
if (config.callbacks?.onError) { |
|||
await config.callbacks.onError(error); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Simulate user notification |
|||
*/ |
|||
private async simulateUserNotification(config: any): Promise<void> { |
|||
this.logger.info('Simulating user notification', { |
|||
title: config.title, |
|||
body: config.body, |
|||
time: config.schedule |
|||
}); |
|||
this.logger.info('User notification simulation completed'); |
|||
} |
|||
} |
Loading…
Reference in new issue