feat: Update test-apps for TimeSafari integration with Endorser.ch API patterns

- 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 safety
This commit is contained in:
Matthew Raymer
2025-09-24 07:38:53 +00:00
parent 999b824a36
commit fe82fd2147
11 changed files with 3432 additions and 389 deletions

View File

@@ -89,15 +89,16 @@
</head>
<body>
<div class="container">
<h1>📱 Daily Notification Plugin - Android Test</h1>
<h1>📱 TimeSafari Daily Notification - Android Test</h1>
<div class="status" id="status">Ready</div>
<div class="button-grid">
<button id="configure">Configure Plugin</button>
<button id="schedule">Schedule Notification</button>
<button id="alarm-status">Check Alarm Status</button>
<button id="request-permission">Request Permission</button>
<button id="configure">Configure TimeSafari</button>
<button id="schedule">Schedule Community Notifications</button>
<button id="endorser-api">Test Endorser.ch API</button>
<button id="callbacks">Register Callbacks</button>
<button id="status">Check Status</button>
<button id="performance">Performance Metrics</button>
</div>

View File

@@ -1,77 +1,47 @@
import { Capacitor } from '@capacitor/core';
import { ConfigLoader, MockDailyNotificationService, TestLogger } from '../shared/config-loader';
// Mock plugin for development
const DailyNotification = {
async configure(options: any) {
console.log('Configure called:', options);
return Promise.resolve();
},
async scheduleDailyNotification(options: any) {
console.log('Schedule called:', options);
return Promise.resolve();
},
async getExactAlarmStatus() {
return Promise.resolve({
supported: true,
enabled: false,
canSchedule: false,
fallbackWindow: '±10 minutes'
});
},
async requestExactAlarmPermission() {
console.log('Request exact alarm permission');
return Promise.resolve();
},
async getPerformanceMetrics() {
return Promise.resolve({
overallScore: 85,
databasePerformance: 90,
memoryEfficiency: 80,
batteryEfficiency: 85,
objectPoolEfficiency: 90,
totalDatabaseQueries: 150,
averageMemoryUsage: 25.5,
objectPoolHits: 45,
backgroundCpuUsage: 2.3,
totalNetworkRequests: 12,
recommendations: ['Enable ETag support', 'Optimize memory usage']
});
}
};
// Test interface
class TestApp {
// Test interface for TimeSafari Android integration
class TimeSafariAndroidTestApp {
private statusElement: HTMLElement;
private logElement: HTMLElement;
private configLoader: ConfigLoader;
private notificationService: MockDailyNotificationService;
private logger: TestLogger;
constructor() {
this.statusElement = document.getElementById('status')!;
this.logElement = document.getElementById('log')!;
this.configLoader = ConfigLoader.getInstance();
this.logger = new TestLogger('debug');
this.notificationService = new MockDailyNotificationService(this.configLoader.getConfig());
this.setupEventListeners();
this.log('Test app initialized');
this.log('TimeSafari Android Test app initialized');
}
private setupEventListeners() {
document.getElementById('configure')?.addEventListener('click', () => this.testConfigure());
document.getElementById('schedule')?.addEventListener('click', () => this.testSchedule());
document.getElementById('alarm-status')?.addEventListener('click', () => this.testAlarmStatus());
document.getElementById('request-permission')?.addEventListener('click', () => this.testRequestPermission());
document.getElementById('endorser-api')?.addEventListener('click', () => this.testEndorserAPI());
document.getElementById('callbacks')?.addEventListener('click', () => this.testCallbacks());
document.getElementById('status')?.addEventListener('click', () => this.testStatus());
document.getElementById('performance')?.addEventListener('click', () => this.testPerformance());
document.getElementById('clear-log')?.addEventListener('click', () => this.clearLog());
}
private async testConfigure() {
try {
this.log('Testing configuration...');
await DailyNotification.configure({
storage: 'shared',
ttlSeconds: 1800,
prefetchLeadMinutes: 15,
enableETagSupport: true,
enableErrorHandling: true,
enablePerformanceOptimization: true
this.log('Testing TimeSafari configuration...');
await this.configLoader.loadConfig();
const config = this.configLoader.getConfig();
await this.notificationService.initialize();
this.log('✅ TimeSafari configuration successful', {
appId: config.timesafari.appId,
appName: config.timesafari.appName,
version: config.timesafari.version
});
this.log('✅ Configuration successful');
this.updateStatus('Configured');
} catch (error) {
this.log(`❌ Configuration failed: ${error}`);
@@ -80,53 +50,271 @@ class TestApp {
private async testSchedule() {
try {
this.log('Testing notification scheduling...');
await DailyNotification.scheduleDailyNotification({
url: 'https://api.example.com/daily-content',
time: '09:00',
title: 'Daily Test Notification',
body: 'This is a test notification from the Android test app'
});
this.log('✅ Notification scheduled successfully');
this.log('Testing TimeSafari community notification scheduling...');
const config = this.configLoader.getConfig();
const dualConfig = {
contentFetch: {
enabled: true,
schedule: config.scheduling.contentFetch.schedule,
url: this.configLoader.getEndorserUrl('notificationsBundle'),
headers: this.configLoader.getAuthHeaders(),
ttlSeconds: 3600,
timeout: 30000,
retryAttempts: 3,
retryDelay: 5000,
callbacks: {
onSuccess: async (data: any) => {
this.log('✅ Content fetch successful', data);
await this.processEndorserNotificationBundle(data);
},
onError: async (error: any) => {
this.log('❌ Content fetch failed', error);
}
}
},
userNotification: {
enabled: true,
schedule: config.scheduling.userNotification.schedule,
title: 'TimeSafari Community Update',
body: 'New offers, projects, people, and items await your attention!',
sound: true,
vibration: true,
priority: 'high',
actions: [
{ id: 'view_offers', title: 'View Offers' },
{ id: 'view_projects', title: 'See Projects' },
{ id: 'view_people', title: 'Check People' },
{ id: 'view_items', title: 'Browse Items' },
{ id: 'dismiss', title: 'Dismiss' }
]
},
relationship: {
autoLink: true,
contentTimeout: 300000,
fallbackBehavior: 'show_default'
}
};
await this.notificationService.scheduleDualNotification(dualConfig);
this.log('✅ Community notification scheduled successfully');
this.updateStatus('Scheduled');
} catch (error) {
this.log(`❌ Scheduling failed: ${error}`);
}
}
private async testAlarmStatus() {
private async testEndorserAPI() {
try {
this.log('Testing exact alarm status...');
const status = await DailyNotification.getExactAlarmStatus();
this.log(`📱 Alarm Status:`, status);
this.updateStatus(`Alarm: ${status.canSchedule ? 'Enabled' : 'Disabled'}`);
this.log('Testing Endorser.ch API integration...');
const config = this.configLoader.getConfig();
const testData = config.testData;
// Test parallel API requests pattern
const requests = [
// Offers to person
fetch(`${this.configLoader.getEndorserUrl('offers')}?recipientId=${testData.userDid}&afterId=${testData.lastKnownOfferId}`, {
headers: this.configLoader.getAuthHeaders()
}),
// Offers to user's projects
fetch(`${this.configLoader.getEndorserUrl('offersToPlans')}?afterId=${testData.lastKnownOfferId}`, {
headers: this.configLoader.getAuthHeaders()
}),
// Changes to starred projects
fetch(this.configLoader.getEndorserUrl('plansLastUpdated'), {
method: 'POST',
headers: this.configLoader.getAuthHeaders(),
body: JSON.stringify({
planIds: testData.starredPlanIds,
afterId: testData.lastKnownPlanId
})
})
];
const [offersToPerson, offersToProjects, starredChanges] = await Promise.all(requests);
const notificationData = {
offersToPerson: await offersToPerson.json(),
offersToProjects: await offersToProjects.json(),
starredChanges: await starredChanges.json()
};
this.log('✅ Endorser.ch API integration successful', {
offersToPerson: notificationData.offersToPerson.data?.length || 0,
offersToProjects: notificationData.offersToProjects.data?.length || 0,
starredChanges: notificationData.starredChanges.data?.length || 0
});
this.updateStatus('API Connected');
} catch (error) {
this.log(`Alarm status check failed: ${error}`);
this.log(`Endorser.ch API test failed: ${error}`);
}
}
private async testRequestPermission() {
private async testCallbacks() {
try {
this.log('Testing permission request...');
await DailyNotification.requestExactAlarmPermission();
this.log('✅ Permission request sent');
this.updateStatus('Permission Requested');
this.log('Testing TimeSafari notification callbacks...');
const config = this.configLoader.getConfig();
// Register offers callback
await this.notificationService.registerCallback('offers', async (event: any) => {
this.log('📨 Offers callback triggered', event);
await this.handleOffersNotification(event);
});
// Register projects callback
await this.notificationService.registerCallback('projects', async (event: any) => {
this.log('📨 Projects callback triggered', event);
await this.handleProjectsNotification(event);
});
// Register people callback
await this.notificationService.registerCallback('people', async (event: any) => {
this.log('📨 People callback triggered', event);
await this.handlePeopleNotification(event);
});
// Register items callback
await this.notificationService.registerCallback('items', async (event: any) => {
this.log('📨 Items callback triggered', event);
await this.handleItemsNotification(event);
});
this.log('✅ All callbacks registered successfully');
this.updateStatus('Callbacks Registered');
} catch (error) {
this.log(`Permission request failed: ${error}`);
this.log(`Callback registration failed: ${error}`);
}
}
private async testStatus() {
try {
this.log('Testing notification status...');
const status = await this.notificationService.getDualScheduleStatus();
this.log('📊 Notification Status:', status);
this.updateStatus(`Status: ${status.contentFetch.enabled ? 'Active' : 'Inactive'}`);
} catch (error) {
this.log(`❌ Status check failed: ${error}`);
}
}
private async testPerformance() {
try {
this.log('Testing performance metrics...');
const metrics = await DailyNotification.getPerformanceMetrics();
this.log(`📊 Performance Metrics:`, metrics);
this.log('Testing Android performance metrics...');
const metrics = {
overallScore: 85,
databasePerformance: 90,
memoryEfficiency: 80,
batteryEfficiency: 85,
objectPoolEfficiency: 90,
totalDatabaseQueries: 150,
averageMemoryUsage: 25.5,
objectPoolHits: 45,
backgroundCpuUsage: 2.3,
totalNetworkRequests: 12,
recommendations: ['Enable ETag support', 'Optimize memory usage']
};
this.log('📊 Android Performance Metrics:', metrics);
this.updateStatus(`Performance: ${metrics.overallScore}/100`);
} catch (error) {
this.log(`❌ Performance check failed: ${error}`);
}
}
/**
* Process Endorser.ch notification bundle using parallel API requests
*/
private async processEndorserNotificationBundle(data: any): Promise<void> {
try {
this.log('Processing Endorser.ch notification bundle...');
// Process each notification type
if (data.offersToPerson?.data?.length > 0) {
await this.handleOffersNotification(data.offersToPerson);
}
if (data.starredChanges?.data?.length > 0) {
await this.handleProjectsNotification(data.starredChanges);
}
this.log('✅ Notification bundle processed successfully');
} catch (error) {
this.log(`❌ Bundle processing failed: ${error}`);
}
}
/**
* Handle offers notification events from Endorser.ch API
*/
private async handleOffersNotification(event: any): Promise<void> {
this.log('Handling offers notification:', event);
if (event.data && event.data.length > 0) {
// Process OfferSummaryArrayMaybeMoreBody format
event.data.forEach((offer: any) => {
this.log('Processing offer:', {
jwtId: offer.jwtId,
handleId: offer.handleId,
offeredByDid: offer.offeredByDid,
recipientDid: offer.recipientDid,
objectDescription: offer.objectDescription
});
});
// Check if there are more offers to fetch
if (event.hitLimit) {
const lastOffer = event.data[event.data.length - 1];
this.log('More offers available, last JWT ID:', lastOffer.jwtId);
}
}
}
/**
* Handle projects notification events from Endorser.ch API
*/
private async handleProjectsNotification(event: any): Promise<void> {
this.log('Handling projects notification:', event);
if (event.data && event.data.length > 0) {
// Process PlanSummaryAndPreviousClaimArrayMaybeMore format
event.data.forEach((planData: any) => {
const { plan, wrappedClaimBefore } = planData;
this.log('Processing project change:', {
jwtId: plan.jwtId,
handleId: plan.handleId,
name: plan.name,
issuerDid: plan.issuerDid,
hasPreviousClaim: !!wrappedClaimBefore
});
});
// Check if there are more project changes to fetch
if (event.hitLimit) {
const lastPlan = event.data[event.data.length - 1];
this.log('More project changes available, last JWT ID:', lastPlan.plan.jwtId);
}
}
}
/**
* Handle people notification events
*/
private async handlePeopleNotification(event: any): Promise<void> {
this.log('Handling people notification:', event);
// Implementation would process people data and update local state
}
/**
* Handle items notification events
*/
private async handleItemsNotification(event: any): Promise<void> {
this.log('Handling items notification:', event);
// Implementation would process items data and update local state
}
private log(message: string, data?: any) {
const timestamp = new Date().toLocaleTimeString();
const logEntry = document.createElement('div');
@@ -150,5 +338,5 @@ class TestApp {
// Initialize app when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
new TestApp();
new TimeSafariAndroidTestApp();
});