// import { Capacitor } from '@capacitor/core'; import { ConfigLoader, MockDailyNotificationService, TestLogger } from '../shared/config-loader'; // Phase 4: Import TimeSafari components import { EndorserAPIClient } from '../shared/typescript/EndorserAPIClient'; import { SecurityManager } from '../shared/typescript/SecurityManager'; import { TimeSafariNotificationManager } from '../shared/typescript/TimeSafariNotificationManager'; import { TimeSafariUser, TimeSafariPreferences, EnhancedTimeSafariNotification, TimeSafariNotificationType } from '../../../src/definitions'; // Generic Polling Interface import { GenericPollingRequest, PollingScheduleConfig, PollingResult, StarredProjectsRequest, StarredProjectsResponse, calculateBackoffDelay, createDefaultOutboxPressureManager } from '../../../packages/polling-contracts/src'; // Enhanced UI components for iOS testing class PermissionManager { private container: HTMLElement; private dialogContainer: HTMLElement; constructor(container: HTMLElement, dialogContainer: HTMLElement) { this.container = container; this.dialogContainer = dialogContainer; } async updateStatus(): Promise { // Mock permission status for iOS testing const mockStatus = { granted: true, notifications: 'granted' as const, backgroundRefresh: 'granted' as const }; this.renderStatus(mockStatus); } private renderStatus(status: any): void { const statusClass = status.granted ? 'status-granted' : 'status-denied'; const statusText = status.granted ? 'Granted' : 'Denied'; this.container.innerHTML = `
${status.granted ? '✓' : '✗'} ${statusText}
Notifications: ${status.notifications}
Background Refresh: ${status.backgroundRefresh}
`; } showPermissionDialog(): void { const dialog = document.createElement('div'); dialog.className = 'dialog-overlay'; dialog.innerHTML = `

Enable Daily Notifications

Get notified about new offers, projects, people, and items in your TimeSafari community.

`; this.dialogContainer.appendChild(dialog); dialog.querySelector('#allow-permissions')?.addEventListener('click', () => { this.hideDialog(); this.updateStatus(); }); dialog.querySelector('#deny-permissions')?.addEventListener('click', () => { this.hideDialog(); }); dialog.querySelector('#never-permissions')?.addEventListener('click', () => { this.hideDialog(); }); } private hideDialog(): void { const dialog = this.dialogContainer.querySelector('.dialog-overlay'); if (dialog) { dialog.remove(); } } } class SettingsPanel { private container: HTMLElement; constructor(container: HTMLElement) { this.container = container; } render(): void { this.container.innerHTML = `
`; } } class StatusDashboard { private container: HTMLElement; constructor(container: HTMLElement) { this.container = container; } render(): void { this.container.innerHTML = `
Overall Status
Active
Next Notification
2h 15m
Last Outcome
Success
Cache Age
1h 23m

Performance

Success Rate: 95%
Error Count: 2
`; } } class ErrorDisplay { private container: HTMLElement; constructor(container: HTMLElement) { this.container = container; } showError(error: Error): void { this.container.innerHTML = `
⚠️

Something went wrong

${error.message}

Error Code: ${error.name}

`; } hide(): void { this.container.innerHTML = ''; } } // Enhanced test interface for TimeSafari iOS integration with Phase 4 components class TimeSafariIOSTestApp { private statusElement: HTMLElement; private logElement: HTMLElement; private configLoader: ConfigLoader; private notificationService: MockDailyNotificationService; private logger: TestLogger; // Phase 4: TimeSafari components private endorserAPIClient: EndorserAPIClient; private securityManager: SecurityManager; private timeSafariNotificationManager: TimeSafariNotificationManager; // UI Components private permissionManager: PermissionManager; private settingsPanel: SettingsPanel; private statusDashboard: StatusDashboard; private errorDisplay: ErrorDisplay; 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()); // Phase 4: Initialize TimeSafari components this.endorserAPIClient = new EndorserAPIClient(this.configLoader.getEndorserAPIConfig()); this.securityManager = new SecurityManager(this.configLoader.getSecurityConfig()); this.timeSafariNotificationManager = new TimeSafariNotificationManager(); // Initialize UI components this.permissionManager = new PermissionManager( document.getElementById('permission-status-container')!, document.getElementById('permission-dialog-container')! ); this.settingsPanel = new SettingsPanel(document.getElementById('settings-container')!); this.statusDashboard = new StatusDashboard(document.getElementById('status-container')!); this.errorDisplay = new ErrorDisplay(document.getElementById('error-container')!); this.setupEventListeners(); this.initializeUI(); this.initializePhase4Components(); this.log('TimeSafari iOS Test app initialized with Phase 4 components'); } private setupEventListeners() { // Original test functionality document.getElementById('configure')?.addEventListener('click', () => this.testConfigure()); document.getElementById('schedule')?.addEventListener('click', () => this.testSchedule()); document.getElementById('rolling-window')?.addEventListener('click', () => this.testRollingWindow()); document.getElementById('endorser-api')?.addEventListener('click', () => this.testEndorserAPI()); document.getElementById('callbacks')?.addEventListener('click', () => this.testCallbacks()); document.getElementById('performance')?.addEventListener('click', () => this.testPerformance()); document.getElementById('clear-log')?.addEventListener('click', () => this.clearLog()); // Enhanced UI functionality document.getElementById('check-permissions')?.addEventListener('click', () => this.checkPermissions()); document.getElementById('request-permissions')?.addEventListener('click', () => this.requestPermissions()); document.getElementById('open-settings')?.addEventListener('click', () => this.openSettings()); document.getElementById('test-notification')?.addEventListener('click', () => this.testNotification()); document.getElementById('check-status')?.addEventListener('click', () => this.testStatus()); document.getElementById('refresh-status')?.addEventListener('click', () => this.refreshStatus()); document.getElementById('background-refresh-status')?.addEventListener('click', () => this.checkBackgroundRefresh()); document.getElementById('bg-task-status')?.addEventListener('click', () => this.checkBGTaskStatus()); // Phase 4: TimeSafari component testing document.getElementById('test-security-manager')?.addEventListener('click', () => this.testSecurityManager()); document.getElementById('test-endorser-api-client')?.addEventListener('click', () => this.testEndorserAPIClient()); document.getElementById('test-notification-manager')?.addEventListener('click', () => this.testTimeSafariNotificationManager()); document.getElementById('test-phase4-integration')?.addEventListener('click', () => this.testPhase4Integration()); // Generic Polling Interface testing document.getElementById('test-generic-polling')?.addEventListener('click', () => this.testGenericPolling()); document.getElementById('test-polling-schedule')?.addEventListener('click', () => this.testPollingSchedule()); document.getElementById('test-polling-results')?.addEventListener('click', () => this.testPollingResults()); // Static Daily Reminder event listeners document.getElementById('schedule-reminder')?.addEventListener('click', () => this.scheduleDailyReminder()); document.getElementById('cancel-reminder')?.addEventListener('click', () => this.cancelDailyReminder()); document.getElementById('get-reminders')?.addEventListener('click', () => this.getScheduledReminders()); document.getElementById('update-reminder')?.addEventListener('click', () => this.updateDailyReminder()); } private async initializeUI(): Promise { try { // Initialize UI components await this.permissionManager.updateStatus(); this.settingsPanel.render(); this.statusDashboard.render(); this.log('✅ Enhanced UI components initialized'); } catch (error) { this.log(`❌ UI initialization failed: ${error}`); this.errorDisplay.showError(error as Error); } } // Phase 4: Initialize TimeSafari components private async initializePhase4Components(): Promise { try { this.log('Initializing Phase 4 TimeSafari components...'); // Initialize SecurityManager with test DID const timeSafariUser = this.configLoader.getTimeSafariUser(); const securityInitialized = await this.securityManager.initialize(timeSafariUser.activeDid); if (securityInitialized) { this.log('✅ SecurityManager initialized successfully'); // Generate JWT token for API authentication const jwt = await this.securityManager.generateJWT({ scope: 'notifications', audience: 'endorser-api' }); if (jwt) { this.endorserAPIClient.setAuthToken(jwt); this.log('✅ JWT token generated and set for EndorserAPI'); } } else { this.log('❌ SecurityManager initialization failed'); } // Initialize TimeSafariNotificationManager const managerInitialized = await this.timeSafariNotificationManager.initialize(timeSafariUser); if (managerInitialized) { this.log('✅ TimeSafariNotificationManager initialized successfully'); } else { this.log('❌ TimeSafariNotificationManager initialization failed'); } this.log('Phase 4 components initialization completed'); } catch (error) { this.log('Error initializing Phase 4 components:', error); this.errorDisplay.showError(error as Error); } } // Enhanced UI methods private async checkPermissions(): Promise { try { this.log('Checking permissions...'); await this.permissionManager.updateStatus(); this.log('✅ Permission status updated'); } catch (error) { this.log(`❌ Permission check failed: ${error}`); this.errorDisplay.showError(error as Error); } } private async requestPermissions(): Promise { try { this.log('Requesting permissions...'); this.permissionManager.showPermissionDialog(); this.log('✅ Permission dialog shown'); } catch (error) { this.log(`❌ Permission request failed: ${error}`); this.errorDisplay.showError(error as Error); } } private async openSettings(): Promise { try { this.log('Opening settings...'); // Mock settings opening this.log('✅ Settings opened (mock)'); } catch (error) { this.log(`❌ Settings open failed: ${error}`); this.errorDisplay.showError(error as Error); } } private async testNotification(): Promise { try { this.log('Sending test notification...'); // Mock test notification this.log('✅ Test notification sent (mock)'); } catch (error) { this.log(`❌ Test notification failed: ${error}`); this.errorDisplay.showError(error as Error); } } private async refreshStatus(): Promise { try { this.log('Refreshing status...'); this.statusDashboard.render(); this.log('✅ Status refreshed'); } catch (error) { this.log(`❌ Status refresh failed: ${error}`); this.errorDisplay.showError(error as Error); } } private async checkBackgroundRefresh(): Promise { try { this.log('Checking background app refresh status...'); // Mock background refresh status this.log('✅ Background app refresh: Enabled (mock)'); } catch (error) { this.log(`❌ Background refresh check failed: ${error}`); this.errorDisplay.showError(error as Error); } } private async checkBGTaskStatus(): Promise { try { this.log('Checking BGTaskScheduler status...'); // Mock BGTaskScheduler status this.log('✅ BGTaskScheduler: Active (mock)'); } catch (error) { this.log(`❌ BGTaskScheduler check failed: ${error}`); this.errorDisplay.showError(error as Error); } } private async testConfigure() { try { this.log('Testing TimeSafari iOS configuration...'); await this.configLoader.loadConfig(); const config = this.configLoader.getConfig(); await this.notificationService.initialize(); this.log('✅ TimeSafari iOS configuration successful', { appId: config.timesafari.appId, appName: config.timesafari.appName, version: config.timesafari.version }); this.updateStatus('Configured'); } catch (error) { this.log(`❌ Configuration failed: ${error}`); } } private async testSchedule() { try { this.log('Testing TimeSafari iOS 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('✅ iOS community notification scheduled successfully'); this.updateStatus('Scheduled'); } catch (error) { this.log(`❌ iOS scheduling failed: ${error}`); } } private async testRollingWindow() { try { this.log('Testing iOS rolling window maintenance...'); // Simulate rolling window maintenance const stats = { stats: '64 pending notifications, 20 daily limit', maintenanceNeeded: false, timeUntilNextMaintenance: 900000 }; this.log('✅ Rolling window maintenance completed', stats); this.updateStatus('Rolling Window Maintained'); } catch (error) { this.log(`❌ Rolling window maintenance failed: ${error}`); } } private async testEndorserAPI() { try { this.log('Testing Endorser.ch API integration on iOS...'); 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 on iOS', { 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(`❌ Endorser.ch API test failed: ${error}`); } } private async testCallbacks() { try { this.log('Testing TimeSafari iOS notification callbacks...'); const config = this.configLoader.getConfig(); // Register offers callback await this.notificationService.registerCallback('offers', async (event: any) => { this.log('📨 iOS Offers callback triggered', event); await this.handleOffersNotification(event); }); // Register projects callback await this.notificationService.registerCallback('projects', async (event: any) => { this.log('📨 iOS Projects callback triggered', event); await this.handleProjectsNotification(event); }); // Register people callback await this.notificationService.registerCallback('people', async (event: any) => { this.log('📨 iOS People callback triggered', event); await this.handlePeopleNotification(event); }); // Register items callback await this.notificationService.registerCallback('items', async (event: any) => { this.log('📨 iOS Items callback triggered', event); await this.handleItemsNotification(event); }); this.log('✅ All iOS callbacks registered successfully'); this.updateStatus('Callbacks Registered'); } catch (error) { this.log(`❌ iOS callback registration failed: ${error}`); } } private async testPerformance() { try { this.log('Testing iOS performance metrics...'); const metrics = { overallScore: 88, databasePerformance: 92, memoryEfficiency: 85, batteryEfficiency: 90, objectPoolEfficiency: 88, totalDatabaseQueries: 120, averageMemoryUsage: 22.3, objectPoolHits: 38, backgroundCpuUsage: 1.8, totalNetworkRequests: 8, recommendations: ['Enable background tasks', 'Optimize memory usage'] }; this.log('📊 iOS 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 { try { this.log('Processing Endorser.ch notification bundle on iOS...'); // 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('✅ iOS notification bundle processed successfully'); } catch (error) { this.log(`❌ iOS bundle processing failed: ${error}`); } } /** * Handle offers notification events from Endorser.ch API */ private async handleOffersNotification(event: any): Promise { this.log('Handling iOS offers notification:', event); if (event.data && event.data.length > 0) { // Process OfferSummaryArrayMaybeMoreBody format event.data.forEach((offer: any) => { this.log('Processing iOS 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 { this.log('Handling iOS 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 iOS 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 { this.log('Handling iOS people notification:', event); // Implementation would process people data and update local state } /** * Handle items notification events */ private async handleItemsNotification(event: any): Promise { this.log('Handling iOS 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'); logEntry.innerHTML = `[${timestamp}] ${message}`; if (data) { logEntry.innerHTML += `
${JSON.stringify(data, null, 2)}
`; } this.logElement.appendChild(logEntry); this.logElement.scrollTop = this.logElement.scrollHeight; } private clearLog() { this.logElement.innerHTML = ''; this.log('Log cleared'); } private updateStatus(status: string) { this.statusElement.textContent = status; } // Phase 4: TimeSafari component test methods private async testSecurityManager(): Promise { try { this.log('🔐 Testing SecurityManager (iOS)...'); const timeSafariUser = this.configLoader.getTimeSafariUser(); // Test JWT generation const jwt = await this.securityManager.generateJWT({ scope: 'test', audience: 'test-api' }); if (jwt) { this.log('✅ JWT generation successful'); this.log('JWT token:', jwt.substring(0, 50) + '...'); // Test JWT verification const claims = await this.securityManager.verifyJWT(jwt); if (claims) { this.log('✅ JWT verification successful'); this.log('Claims:', claims); } else { this.log('❌ JWT verification failed'); } } else { this.log('❌ JWT generation failed'); } // Test operation history const history = this.securityManager.getOperationHistory(); this.log(`Security operations performed: ${history.length}`); this.log('SecurityManager test completed'); } catch (error) { this.log('SecurityManager test failed:', error); this.errorDisplay.showError(error as Error); } } private async testEndorserAPIClient(): Promise { try { this.log('🌐 Testing EndorserAPIClient (iOS)...'); const timeSafariUser = this.configLoader.getTimeSafariUser(); // Test offers to person this.log('Testing offers to person...'); const offersResponse = await this.endorserAPIClient.fetchOffersToPerson( timeSafariUser.activeDid, timeSafariUser.lastKnownOfferId, undefined ); this.log(`✅ Offers fetched: ${offersResponse.data.length} offers`); if (offersResponse.data.length > 0) { this.log('Sample offer:', offersResponse.data[0]); } // Test offers to projects this.log('Testing offers to projects...'); const projectsResponse = await this.endorserAPIClient.fetchOffersToProjectsOwnedByMe( timeSafariUser.lastKnownOfferId ); this.log(`✅ Project offers fetched: ${projectsResponse.data.length} offers`); // Test project updates if (timeSafariUser.starredPlanIds && timeSafariUser.starredPlanIds.length > 0) { this.log('Testing project updates...'); const updatesResponse = await this.endorserAPIClient.fetchProjectsLastUpdated( timeSafariUser.starredPlanIds, timeSafariUser.lastKnownPlanId, undefined ); this.log(`✅ Project updates fetched: ${updatesResponse.data.length} updates`); } this.log('EndorserAPIClient test completed'); } catch (error) { this.log('EndorserAPIClient test failed:', error); this.errorDisplay.showError(error as Error); } } private async testTimeSafariNotificationManager(): Promise { try { this.log('📱 Testing TimeSafariNotificationManager (iOS)...'); // Test notification generation this.log('Generating TimeSafari notifications...'); const notifications = await this.timeSafariNotificationManager.generateNotifications({ forceFetch: false, includeMetadata: true, filterByPriority: true, maxNotifications: 10, cacheTtl: 300000 }); this.log(`✅ Generated ${notifications.length} notifications`); // Display notification details notifications.forEach((notification, index) => { this.log(`Notification ${index + 1}:`, { type: notification.type, subtype: notification.subtype, priority: notification.notificationPriority, timestamp: new Date(notification.timestamp).toISOString(), disabled: notification.disabled, sound: notification.sound, vibration: notification.vibration }); }); // Test statistics const stats = this.timeSafariNotificationManager.getStatistics(); this.log('Manager statistics:', stats); this.log('TimeSafariNotificationManager test completed'); } catch (error) { this.log('TimeSafariNotificationManager test failed:', error); this.errorDisplay.showError(error as Error); } } private async testPhase4Integration(): Promise { try { this.log('🚀 Testing complete Phase 4 integration (iOS)...'); const timeSafariUser = this.configLoader.getTimeSafariUser(); // Test complete workflow this.log('Step 1: Security authentication...'); const jwt = await this.securityManager.generateJWT({ scope: 'notifications', audience: 'endorser-api' }); if (!jwt) { throw new Error('JWT generation failed'); } this.endorserAPIClient.setAuthToken(jwt); this.log('✅ Authentication successful'); this.log('Step 2: Fetching TimeSafari data...'); const bundle = await this.endorserAPIClient.fetchAllTimeSafariNotifications({ activeDid: timeSafariUser.activeDid, starredPlanIds: timeSafariUser.starredPlanIds || [], lastKnownOfferId: timeSafariUser.lastKnownOfferId, lastKnownPlanId: timeSafariUser.lastKnownPlanId, fetchOffersToPerson: true, fetchOffersToProjects: true, fetchProjectUpdates: true, notificationPreferences: { offers: true, projects: true, people: false, items: true } }); this.log(`✅ Data fetched successfully: ${bundle.success}`); this.log('Bundle metadata:', bundle.metadata); this.log('Step 3: Generating notifications...'); const notifications = await this.timeSafariNotificationManager.generateNotifications({ forceFetch: true, includeMetadata: true, maxNotifications: 20 }); this.log(`✅ Generated ${notifications.length} notifications`); // Summary this.log('🎉 Phase 4 integration test completed successfully!'); this.log('Summary:', { securityInitialized: this.securityManager.isInitialized(), apiClientReady: !!this.endorserAPIClient, managerInitialized: this.timeSafariNotificationManager.isInitialized(), notificationsGenerated: notifications.length, bundleSuccess: bundle.success }); } catch (error) { this.log('Phase 4 integration test failed:', error); this.errorDisplay.showError(error as Error); } } // Generic Polling Interface Test Methods private async testGenericPolling(): Promise { try { this.log('🔄 Testing Generic Polling Interface (iOS)...'); // Create a starred projects polling request const starredProjectsRequest: GenericPollingRequest = { endpoint: '/api/v2/report/plansLastUpdatedBetween', method: 'POST', headers: { 'Content-Type': 'application/json', 'User-Agent': 'TimeSafari-DailyNotificationPlugin/1.0.0', 'Authorization': `Bearer ${await this.getJwtToken()}` }, body: { planIds: ['plan:test-1', 'plan:test-2'], afterId: '1704067200_abc123_12345678', limit: 100 }, responseSchema: { validate: (data: any): data is StarredProjectsResponse => { return data && Array.isArray(data.data) && typeof data.hitLimit === 'boolean' && data.pagination && typeof data.pagination.hasMore === 'boolean'; }, transformError: (error: any) => ({ code: 'VALIDATION_ERROR', message: error.message || 'Validation failed', retryable: false }) }, retryConfig: { maxAttempts: 3, backoffStrategy: 'exponential', baseDelayMs: 1000 }, timeoutMs: 30000 }; this.log('✅ Generic polling request created successfully'); this.log('Request details:', { endpoint: starredProjectsRequest.endpoint, method: starredProjectsRequest.method, planIds: starredProjectsRequest.body.planIds, afterId: starredProjectsRequest.body.afterId }); // Test backoff calculation const backoffDelay = calculateBackoffDelay(1, starredProjectsRequest.retryConfig); this.log(`✅ Backoff delay calculated: ${backoffDelay}ms`); // Test outbox pressure manager const pressureManager = createDefaultOutboxPressureManager(); const pressureStatus = await pressureManager.checkStoragePressure(50); this.log('✅ Outbox pressure check:', pressureStatus); this.log('Generic Polling Interface test completed successfully'); } catch (error) { this.log('Generic Polling Interface test failed:', error); this.errorDisplay.showError(error as Error); } } private async testPollingSchedule(): Promise { try { this.log('📅 Testing Polling Schedule Configuration (iOS)...'); const timeSafariUser = this.configLoader.getTimeSafariUser(); // Create schedule configuration const scheduleConfig: PollingScheduleConfig = { request: { endpoint: '/api/v2/report/plansLastUpdatedBetween', method: 'POST', headers: { 'Content-Type': 'application/json', 'User-Agent': 'TimeSafari-DailyNotificationPlugin/1.0.0', 'Authorization': `Bearer ${await this.getJwtToken()}` }, body: { planIds: timeSafariUser.starredPlanIds || [], afterId: timeSafariUser.lastKnownPlanId, limit: 100 }, responseSchema: { validate: (data: any): data is StarredProjectsResponse => { return data && Array.isArray(data.data); }, transformError: (error: any) => ({ code: 'VALIDATION_ERROR', message: error.message, retryable: false }) }, retryConfig: { maxAttempts: 3, backoffStrategy: 'exponential', baseDelayMs: 1000 }, timeoutMs: 30000 }, schedule: { cronExpression: '0 10,16 * * *', // 10 AM and 4 PM daily timezone: 'UTC', maxConcurrentPolls: 1 }, notificationConfig: { enabled: true, templates: { singleUpdate: '{projectName} has been updated', multipleUpdates: 'You have {count} new updates in your starred projects' }, groupingRules: { maxGroupSize: 5, timeWindowMinutes: 5 } }, stateConfig: { watermarkKey: 'lastAckedStarredPlanChangesJwtId', storageAdapter: 'timesafari' } }; this.log('✅ Polling schedule configuration created successfully'); this.log('Schedule details:', { cronExpression: scheduleConfig.schedule.cronExpression, timezone: scheduleConfig.schedule.timezone, maxConcurrentPolls: scheduleConfig.schedule.maxConcurrentPolls, watermarkKey: scheduleConfig.stateConfig.watermarkKey }); // Mock scheduling the poll this.log('📅 Scheduling polling (mock)...'); const scheduleId = `ios-poll-${Date.now()}`; this.log(`✅ Polling scheduled with ID: ${scheduleId}`); this.log('Polling Schedule test completed successfully'); } catch (error) { this.log('Polling Schedule test failed:', error); this.errorDisplay.showError(error as Error); } } private async testPollingResults(): Promise { try { this.log('📊 Testing Polling Results Handling (iOS)...'); // Mock polling result const mockResult: PollingResult = { success: true, data: { data: [ { planSummary: { jwtId: '1704153600_mno345_87654321', handleId: 'test-project-1', name: 'Test Project 1', issuerDid: 'did:example:test-issuer', locLat: 40.7128, locLon: -74.0060, url: 'https://test-project-1.com', version: '1.0.0' }, previousClaim: { jwtId: '1704067200_abc123_12345678', claimType: 'project_update', claimData: { status: 'in_progress', progress: 0.75 }, metadata: { createdAt: '2025-01-01T10:00:00Z', updatedAt: '2025-01-01T12:00:00Z' } } } ], hitLimit: false, pagination: { hasMore: false, nextAfterId: null } }, metadata: { requestId: 'req-ios-test-123', timestamp: new Date().toISOString(), duration: 1250, attempt: 1 } }; this.log('✅ Mock polling result created'); this.log('Result details:', { success: mockResult.success, dataCount: mockResult.data?.data.length || 0, hitLimit: mockResult.data?.hitLimit, hasMore: mockResult.data?.pagination.hasMore, duration: mockResult.metadata?.duration }); // Test result processing if (mockResult.success && mockResult.data) { const changes = mockResult.data.data; if (changes.length > 0) { this.log('📝 Processing polling results...'); // Generate notifications this.log(`✅ Generated notifications for ${changes.length} changes`); // Update watermark with CAS const latestJwtId = changes[changes.length - 1].planSummary.jwtId; this.log(`✅ Updated watermark to: ${latestJwtId}`); // Acknowledge changes with server const jwtIds = changes.map(c => c.planSummary.jwtId); this.log(`✅ Acknowledged ${jwtIds.length} changes with server`); } } this.log('Polling Results test completed successfully'); } catch (error) { this.log('Polling Results test failed:', error); this.errorDisplay.showError(error as Error); } } private async getJwtToken(): Promise { // Mock JWT token generation return 'mock-jwt-token-ios-test'; } // Static Daily Reminder Methods private async scheduleDailyReminder(): Promise { try { this.log('Scheduling daily reminder...'); const reminderOptions = { id: (document.getElementById('reminder-id') as HTMLInputElement)?.value || 'morning_checkin', title: (document.getElementById('reminder-title') as HTMLInputElement)?.value || 'Good Morning!', body: (document.getElementById('reminder-body') as HTMLInputElement)?.value || 'Time to check your TimeSafari community updates', time: (document.getElementById('reminder-time') as HTMLInputElement)?.value || '09:00', sound: (document.getElementById('reminder-sound') as HTMLInputElement)?.checked ?? true, vibration: (document.getElementById('reminder-vibration') as HTMLInputElement)?.checked ?? true, priority: (document.getElementById('reminder-priority') as HTMLSelectElement)?.value || 'normal', repeatDaily: (document.getElementById('reminder-repeat') as HTMLInputElement)?.checked ?? true }; this.log('Reminder options:', reminderOptions); await this.plugin.scheduleDailyReminder(reminderOptions); this.log('✅ Daily reminder scheduled successfully'); this.errorDisplay.showSuccess('Daily reminder scheduled successfully!'); } catch (error) { this.log('❌ Failed to schedule daily reminder:', error); this.errorDisplay.showError(error as Error); } } private async cancelDailyReminder(): Promise { try { this.log('Cancelling daily reminder...'); const reminderId = (document.getElementById('reminder-id') as HTMLInputElement)?.value || 'morning_checkin'; await this.plugin.cancelDailyReminder(reminderId); this.log('✅ Daily reminder cancelled successfully'); this.errorDisplay.showSuccess('Daily reminder cancelled successfully!'); } catch (error) { this.log('❌ Failed to cancel daily reminder:', error); this.errorDisplay.showError(error as Error); } } private async getScheduledReminders(): Promise { try { this.log('Getting scheduled reminders...'); const result = await this.plugin.getScheduledReminders(); this.log('✅ Scheduled reminders retrieved:', result); if (result.reminders && result.reminders.length > 0) { this.errorDisplay.showSuccess(`Found ${result.reminders.length} scheduled reminders`); console.table(result.reminders); } else { this.errorDisplay.showInfo('No scheduled reminders found'); } } catch (error) { this.log('❌ Failed to get scheduled reminders:', error); this.errorDisplay.showError(error as Error); } } private async updateDailyReminder(): Promise { try { this.log('Updating daily reminder...'); const reminderId = (document.getElementById('reminder-id') as HTMLInputElement)?.value || 'morning_checkin'; const updateOptions = { title: (document.getElementById('reminder-title') as HTMLInputElement)?.value, body: (document.getElementById('reminder-body') as HTMLInputElement)?.value, time: (document.getElementById('reminder-time') as HTMLInputElement)?.value, sound: (document.getElementById('reminder-sound') as HTMLInputElement)?.checked, vibration: (document.getElementById('reminder-vibration') as HTMLInputElement)?.checked, priority: (document.getElementById('reminder-priority') as HTMLSelectElement)?.value, repeatDaily: (document.getElementById('reminder-repeat') as HTMLInputElement)?.checked }; this.log('Update options:', updateOptions); await this.plugin.updateDailyReminder(reminderId, updateOptions); this.log('✅ Daily reminder updated successfully'); this.errorDisplay.showSuccess('Daily reminder updated successfully!'); } catch (error) { this.log('❌ Failed to update daily reminder:', error); this.errorDisplay.showError(error as Error); } } } // Initialize app when DOM is ready document.addEventListener('DOMContentLoaded', () => { new TimeSafariIOSTestApp(); });