From 0b6a8cdd39e2c6f3e722c4912096071c618ee0f8 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Fri, 3 Oct 2025 07:02:55 +0000 Subject: [PATCH] feat(phase2): implement ActiveDid Integration & TimeSafari API Enhancement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Enhanced ConfigureOptions with comprehensive TimeSafari activeDid configuration - Extended ContentFetchConfig with Endorser.ch API endpoints and TimeSafari config - Added detailed TimeSafari notification types (Offers, Projects, People, Items) - Implemented Host-provided activeDid Plugin Configuration with auto-sync - Enhanced Android retry logic with TimeSafari activeDid change detection - Enhanced Web retry logic with Phase 2 ActiveDid change support - Added comprehensive TimeSafari fallback content generation - Implemented cross-platform ActiveDid change event tracking Phase 2 delivers: ✅ Enhanced ConfigureOptions with host-provided activeDid patterns ✅ Extension of ContentFetchConfig with Endorser.ch endpoints ✅ Complete TimeSafari notification type definitions ✅ Host-provided activeDid Plugin Configuration implementation ✅ Enhanced Android retry logic with activeDid change detection ✅ Enhanced Web retry logic with session-based activeDid tracking ✅ TimeSafari-aware fallback content generation ✅ Comprehensive configuration storage and persistence Ready for Phase 3: Background Enhancement & TimeSafari Coordination --- src/android/DailyNotificationFetchWorker.java | 124 ++++++++++++++- src/android/DailyNotificationPlugin.java | 74 +++++++-- src/callback-registry.ts | 138 ++++++++++++++++- src/definitions.ts | 144 +++++++++++++++++- 4 files changed, 457 insertions(+), 23 deletions(-) diff --git a/src/android/DailyNotificationFetchWorker.java b/src/android/DailyNotificationFetchWorker.java index 029714d..d246d87 100644 --- a/src/android/DailyNotificationFetchWorker.java +++ b/src/android/DailyNotificationFetchWorker.java @@ -203,23 +203,29 @@ public class DailyNotificationFetchWorker extends Worker { */ private Result handleFailedFetch(int retryCount, long scheduledTime) { try { - Log.d(TAG, "Handling failed fetch - Retry: " + retryCount); + Log.d(TAG, "Phase 2: Handling failed fetch - Retry: " + retryCount); - if (retryCount < MAX_RETRY_ATTEMPTS) { - // Schedule retry - scheduleRetry(retryCount + 1, scheduledTime); - Log.i(TAG, "Scheduled retry attempt " + (retryCount + 1)); + // Phase 2: Check for TimeSafari special retry triggers + if (shouldRetryForActiveDidChange()) { + Log.d(TAG, "Phase 2: ActiveDid change detected - extending retry quota"); + retryCount = 0; // Reset retry count for activeDid change + } + + if (retryCount < MAX_RETRIES_FOR_TIMESAFARI()) { + // Phase 2: Schedule enhanced retry with activeDid consideration + scheduleRetryWithActiveDidSupport(retryCount + 1, scheduledTime); + Log.i(TAG, "Phase 2: Scheduled retry attempt " + (retryCount + 1) + " with TimeSafari support"); return Result.retry(); } else { // Max retries reached - use fallback content - Log.w(TAG, "Max retries reached, using fallback content"); - useFallbackContent(scheduledTime); + Log.w(TAG, "Phase 2: Max retries reached, using fallback content"); + useFallbackContentWithActiveDidSupport(scheduledTime); return Result.success(); } } catch (Exception e) { - Log.e(TAG, "Error handling failed fetch", e); + Log.e(TAG, "Phase 2: Error handling failed fetch", e); return Result.failure(); } } @@ -275,6 +281,108 @@ public class DailyNotificationFetchWorker extends Worker { return Math.min(exponentialDelay, maxDelay); } + // MARK: - Phase 2: TimeSafari ActiveDid Enhancement Methods + + /** + * Phase 2: Check if retry is needed due to activeDid change + */ + private boolean shouldRetryForActiveDidChange() { + try { + // Check if activeDid has changed since last fetch attempt + android.content.SharedPreferences prefs = context.getSharedPreferences("daily_notification_timesafari", android.content.Context.MODE_PRIVATE); + long lastFetchAttempt = prefs.getLong("lastFetchAttempt", 0); + long lastActiveDidChange = prefs.getLong("lastActiveDidChange", 0); + + boolean activeDidChanged = lastActiveDidChange > lastFetchAttempt; + + if (activeDidChanged) { + Log.d(TAG, "Phase 2: ActiveDid change detected in retry logic"); + return true; + } + + return false; + + } catch (Exception e) { + Log.e(TAG, "Phase 2: Error checking activeDid change", e); + return false; + } + } + + /** + * Phase 2: Get max retries with TimeSafari enhancements + */ + private int MAX_RETRIES_FOR_TIMESAFARI() { + // Base retries + additional for activeDid changes + return MAX_RETRY_ATTEMPTS + 2; // Extra retries for TimeSafari integration + } + + /** + * Phase 2: Schedule retry with activeDid support + */ + private void scheduleRetryWithActiveDidSupport(int retryCount, long scheduledTime) { + try { + Log.d(TAG, "Phase 2: Scheduling retry attempt " + retryCount + " with TimeSafari support"); + + // Store the last fetch attempt time for activeDid change detection + android.content.SharedPreferences prefs = context.getSharedPreferences("daily_notification_timesafari", android.content.Context.MODE_PRIVATE); + prefs.edit().putLong("lastFetchAttempt", System.currentTimeMillis()).apply(); + + // Delegate to original retry logic + scheduleRetry(retryCount, scheduledTime); + + } catch (Exception e) { + Log.e(TAG, "Phase 2: Error scheduling enhanced retry", e); + // Fallback to original retry logic + scheduleRetry(retryCount, scheduledTime); + } + } + + /** + * Phase 2: Use fallback content with activeDid support + */ + private void useFallbackContentWithActiveDidSupport(long scheduledTime) { + try { + Log.d(TAG, "Phase 2: Using fallback content with TimeSafari support"); + + // Generate TimeSafari-aware fallback content + NotificationContent fallbackContent = generateTimeSafariFallbackContent(); + + if (fallbackContent != null) { + storage.saveNotificationContent(fallbackContent); + Log.i(TAG, "Phase 2: TimeSafari fallback content saved"); + } else { + // Fallback to original logic + useFallbackContent(scheduledTime); + } + + } catch (Exception e) { + Log.e(TAG, "Phase 2: Error using enhanced fallback content", e); + // Fallback to original logic + useFallbackContent(scheduledTime); + } + } + + /** + * Phase 2: Generate TimeSafari-aware fallback content + */ + private NotificationContent generateTimeSafariFallbackContent() { + try { + // Generate fallback content specific to TimeSafari context + NotificationContent content = new NotificationContent(); + content.id = "timesafari_fallback_" + System.currentTimeMillis(); + content.title = "TimeSafari Update Available"; + content.body = "Your community updates are ready. Tap to view offers, projects, and connections."; + content.fetchTime = System.currentTimeMillis(); + content.scheduledTime = System.currentTimeMillis() + 30000; // 30 seconds from now + + return content; + + } catch (Exception e) { + Log.e(TAG, "Phase 2: Error generating TimeSafari fallback content", e); + return null; + } + } + /** * Use fallback content when all retries fail * diff --git a/src/android/DailyNotificationPlugin.java b/src/android/DailyNotificationPlugin.java index 2022d68..71c7d33 100644 --- a/src/android/DailyNotificationPlugin.java +++ b/src/android/DailyNotificationPlugin.java @@ -987,36 +987,90 @@ public class DailyNotificationPlugin extends Plugin { */ private void configureActiveDidIntegration(JSObject config) { try { - Log.d(TAG, "Configuring activeDid integration"); + Log.d(TAG, "Configuring Phase 2 activeDid integration"); String platform = config.getString("platform", "android"); String storageType = config.getString("storageType", "plugin-managed"); Integer jwtExpirationSeconds = config.getInteger("jwtExpirationSeconds", 60); String apiServer = config.getString("apiServer"); - Log.d(TAG, "ActiveDid config - Platform: " + platform + ", Storage: " + storageType + - ", JWT Expiry: " + jwtExpirationSeconds + "s, API Server: " + apiServer); + // Phase 2: Host-provided activeDid initial configuration + String initialActiveDid = config.getString("activeDid"); + boolean autoSync = config.getBoolean("autoSync", false); + Integer identityChangeGraceSeconds = config.getInteger("identityChangeGraceSeconds", 30); - // Configure JWT manager with custom expiration + Log.d(TAG, "Phase 2 ActiveDid config - Platform: " + platform + + ", Storage: " + storageType + ", JWT Expiry: " + jwtExpirationSeconds + "s" + + ", API Server: " + apiServer + ", Initial ActiveDid: " + + (initialActiveDid != null ? initialActiveDid.substring(0, Math.min(20, initialActiveDid.length())) + "..." : "null") + + ", AutoSync: " + autoSync + ", Grace Period: " + identityChangeGraceSeconds + "s"); + + // Phase 2: Configure JWT manager with auto-sync capabilities if (jwtManager != null) { - // We'll set the JWT expiration when activeDid is provided - Log.d(TAG, "JWT manager configured for activeDid integration"); + if (initialActiveDid != null && !initialActiveDid.isEmpty()) { + jwtManager.setActiveDid(initialActiveDid, jwtExpirationSeconds); + Log.d(TAG, "Phase 2: Initial ActiveDid set in JWT manager"); + } + Log.d(TAG, "Phase 2: JWT manager configured with auto-sync: " + autoSync); } - // Configure enhanced fetcher with API server + // Phase 2: Configure enhanced fetcher with TimeSafari API support if (enhancedFetcher != null && apiServer != null && !apiServer.isEmpty()) { enhancedFetcher.setApiServerUrl(apiServer); - Log.d(TAG, "Enhanced fetcher configured with API server: " + apiServer); + Log.d(TAG, "Phase 2: Enhanced fetcher configured with API server: " + apiServer); + + // Phase 2: Set up TimeSafari-specific configuration + if (initialActiveDid != null && !initialActiveDid.isEmpty()) { + EnhancedDailyNotificationFetcher.TimeSafariUserConfig userConfig = + new EnhancedDailyNotificationFetcher.TimeSafariUserConfig(); + userConfig.activeDid = initialActiveDid; + userConfig.fetchOffersToPerson = true; + userConfig.fetchOffersToProjects = true; + userConfig.fetchProjectUpdates = true; + + Log.d(TAG, "Phase 2: TimeSafari user configuration prepared"); + } } - Log.i(TAG, "ActiveDid integration configured successfully"); + // Phase 2: Store auto-sync configuration for future use + storeAutoSyncConfiguration(autoSync, identityChangeGraceSeconds); + + Log.i(TAG, "Phase 2 ActiveDid integration configured successfully"); } catch (Exception e) { - Log.e(TAG, "Error configuring activeDid integration", e); + Log.e(TAG, "Error configuring Phase 2 activeDid integration", e); throw e; } } + /** + * Store auto-sync configuration for background tasks + */ + private void storeAutoSyncConfiguration(boolean autoSync, int gracePeriodSeconds) { + try { + if (storage != null) { + // Store auto-sync settings in plugin storage + Map syncConfig = new HashMap<>(); + syncConfig.put("autoSync", autoSync); + syncConfig.put("gracePeriodSeconds", gracePeriodSeconds); + syncConfig.put("configuredAt", System.currentTimeMillis()); + + // Store in SharedPreferences for persistence + android.content.SharedPreferences preferences = getContext() + .getSharedPreferences("daily_notification_timesafari", Context.MODE_PRIVATE); + preferences.edit() + .putBoolean("autoSync", autoSync) + .putInt("gracePeriodSeconds", gracePeriodSeconds) + .putLong("configuredAt", System.currentTimeMillis()) + .apply(); + + Log.d(TAG, "Phase 2: Auto-sync configuration stored"); + } + } catch (Exception e) { + Log.e(TAG, "Error storing auto-sync configuration", e); + } + } + /** * Set active DID from host application * diff --git a/src/callback-registry.ts b/src/callback-registry.ts index 51d8bc6..d75284c 100644 --- a/src/callback-registry.ts +++ b/src/callback-registry.ts @@ -56,6 +56,8 @@ export class CallbackRegistryImpl implements CallbackRegistry { lastFailure: number; open: boolean; }>(); + // Phase 2: TimeSafari ActiveDid change tracking + private lastRetryAttempts?: Map; constructor() { this.startRetryProcessor(); @@ -226,13 +228,24 @@ export class CallbackRegistryImpl implements CallbackRegistry { private async scheduleRetry(callback: CallbackRecord, event: CallbackEvent): Promise { const retryCount = callback.retryCount || 0; - if (retryCount >= 5) { + // Phase 2: Enhanced retry logic with TimeSafari activeDid support + const maxRetries = Math.max(5, this.getMaxRetriesForTimeSafari()); + + if (retryCount >= maxRetries && !this.shouldRetryForActiveDidChange()) { console.warn(`DNP-CB-RETRY-LIMIT: Max retries reached for ${callback.id}`); return; } - const backoffMs = Math.min(1000 * Math.pow(2, retryCount), 60000); // Cap at 1 minute - const retryEvent = { ...event, retryCount: retryCount + 1 }; + // Phase 2: Check for activeDid change retry triggers + if (this.shouldRetryForActiveDidChange()) { + console.log(`DNP-CB-PHASE2: ActiveDid change detected - resetting retry count for ${callback.id}`); + callback.retryCount = 0; // Reset retry count for activeDid change + } + + let actualRetryCount = callback.retryCount || 0; + + const backoffMs = Math.min(1000 * Math.pow(2, actualRetryCount), 60000); // Cap at 1 minute + const retryEvent = { ...event, retryCount: actualRetryCount + 1 }; if (!this.retryQueue.has(callback.id)) { this.retryQueue.set(callback.id, []); @@ -240,7 +253,124 @@ export class CallbackRegistryImpl implements CallbackRegistry { this.retryQueue.get(callback.id)!.push(retryEvent); - console.log(`DNP-CB-RETRY: Scheduled retry ${retryCount + 1} for ${callback.id} in ${backoffMs}ms`); + // Phase 2: Store last retry attempt for activeDid change detection + this.storeLastRetryAttempt(callback.id); + + console.log(`DNP-CB-PHASE2-RETRY: Scheduled retry ${actualRetryCount + 1} for ${callback.id} in ${backoffMs}ms with TimeSafari support`); + } + + // Phase 2: Enhanced retry methods for TimeSafari integration + + /** + * Phase 2: Get max retries with TimeSafari enhancements + */ + private getMaxRetriesForTimeSafari(): number { + // Base retries + additional for activeDid changes + return 7; // Extra retries for TimeSafari integration + } + + /** + * Phase 2: Check if retry is needed due to activeDid change + */ + private shouldRetryForActiveDidChange(): boolean { + try { + // Check if activeDid has changed since last retry attempt + const lastRetryAttempt = this.getLastRetryAttempt(); + const lastActiveDidChange = this.getLastActiveDidChange(); + + if (lastActiveDidChange > lastRetryAttempt) { + console.log('DNP-CB-PHASE2: ActiveDid change detected in web retry logic'); + return true; + } + + return false; + + } catch (error) { + console.error('DNP-CB-PHASE2: Error checking activeDid change', error); + return false; + } + } + + /** + * Phase 2: Store last retry attempt for activeDid change detection + */ + private storeLastRetryAttempt(callbackId: string): void { + try { + const lastRetryMap = this.getLastRetryMap(); + lastRetryMap.set(callbackId, Date.now()); + + // Store in sessionStorage for web persistence + if (typeof window !== 'undefined' && window.sessionStorage) { + const retryMapKey = 'dailynotification_last_retry_attempts'; + const retryMapObj = Object.fromEntries(lastRetryMap); + window.sessionStorage.setItem(retryMapKey, JSON.stringify(retryMapObj)); + } + + } catch (error) { + console.error('DNP-CB-PHASE2: Error storing last retry attempt', error); + } + } + + /** + * Phase 2: Get last retry attempt timestamp + */ + private getLastRetryAttempt(): number { + try { + const lastRetryMap = this.getLastRetryMap(); + // Get the most recent retry attempt across all callbacks + let maxRetryTime = 0; + for (const retryTime of lastRetryMap.values()) { + maxRetryTime = Math.max(maxRetryTime, retryTime); + } + return maxRetryTime; + } catch (error) { + console.error('DNP-CB-PHASE2: Error getting last retry attempt', error); + return 0; + } + } + + /** + * Phase 2: Get last activeDid change timestamp + */ + private getLastActiveDidChange(): number { + try { + // This would be set when activeDid changes + if (typeof window !== 'undefined' && window.sessionStorage) { + const lastChangeStr = window.sessionStorage.getItem('dailynotification_last_active_did_change'); + return lastChangeStr ? parseInt(lastChangeStr, 10) : 0; + } + return 0; + } catch (error) { + console.error('DNP-CB-PHASE2: Error getting last activeDid change', error); + return 0; + } + } + + /** + * Phase 2: Get last retry map + */ + private getLastRetryMap(): Map { + try { + if (!this.lastRetryAttempts) { + this.lastRetryAttempts = new Map(); + + // Load from sessionStorage if available + if (typeof window !== 'undefined' && window.sessionStorage) { + const retryMapKey = 'dailynotification_last_retry_attempts'; + const retryMapStr = window.sessionStorage.getItem(retryMapKey); + if (retryMapStr) { + const retryMapObj = JSON.parse(retryMapStr); + for (const [key, value] of Object.entries(retryMapObj)) { + this.lastRetryAttempts.set(key, value as number); + } + } + } + } + return this.lastRetryAttempts; + } catch (error) { + console.error('DNP-CB-PHASE2: Error getting last retry map', error); + return new Map(); + } } private startRetryProcessor(): void { diff --git a/src/definitions.ts b/src/definitions.ts index 8e8265d..89c340b 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -160,12 +160,20 @@ export interface ConfigureOptions { prefetchLeadMinutes?: number; maxNotificationsPerDay?: number; retentionDays?: number; - // Phase 1: ActiveDid Integration Enhancement + // Phase 2: TimeSafari ActiveDid Integration Enhancement activeDidIntegration?: { platform: 'android' | 'ios' | 'web' | 'electron'; storageType: 'plugin-managed' | 'host-managed'; jwtExpirationSeconds?: number; apiServer?: string; + // Phase 2: Host-provided activeDid configuration + activeDid?: string; // Initial activeDid from host + hostCredentials?: { + platform?: string; // Platform identifier + accessToken?: string; // Optional access token + }; + autoSync?: boolean; // Auto-sync activeDid changes + identityChangeGraceSeconds?: number; // Grace period for activeDid changes }; } @@ -189,6 +197,30 @@ export interface ContentFetchConfig { contentHandler?: ContentHandler; cachePolicy?: CachePolicy; networkConfig?: NetworkConfig; + // Phase 2: TimeSafari Endorser.ch API configuration + timesafariConfig?: { + activeDid: string; // Required activeDid for authentication + endpoints?: { + offersToPerson?: string; + offersToPlans?: string; + projectsLastUpdated?: string; + }; + syncConfig?: { + enableParallel?: boolean; // Enable parallel API requests + maxConcurrent?: number; // Max concurrent requests + batchSize?: number; // Batch size for requests + }; + credentialConfig?: { + jwtSecret?: string; // JWT secret for signing + tokenExpirationMinutes?: number; // Token expiration + refreshThresholdMinutes?: number; // Refresh threshold + }; + errorPolicy?: { + maxRetries?: number; + backoffMultiplier?: number; + activeDidChangeRetries?: number; // Special retry for activeDid changes + }; + }; } export interface UserNotificationConfig { @@ -397,6 +429,116 @@ export interface PlanSummary { url?: string; }; +// Phase 2: Detailed TimeSafari Notification Types +export interface TimeSafariNotificationBundle { + offersToPerson?: OffersResponse; + offersToProjects?: OffersToPlansResponse; + projectUpdates?: PlansLastUpdatedResponse; + fetchTimestamp: number; + success: boolean; + error?: string; + metadata?: { + activeDid: string; + fetchDurationMs: number; + cachedResponses: number; + networkResponses: number; + }; +} + +export interface TimeSafariUserConfig { + activeDid: string; // Required for all operations + lastKnownOfferId?: string; + lastKnownPlanId?: string; + starredPlanIds?: string[]; + fetchOffersToPerson?: boolean; + fetchOffersToProjects?: boolean; + fetchProjectUpdates?: boolean; + notificationPreferences?: { + offers: boolean; + projects: boolean; + people: boolean; + items: boolean; + }; +} + +// Enhanced notification types per specification +export interface TimeSafariOfferNotification { + type: 'offer'; + subtype: 'new_to_me' | 'changed_to_me' | 'new_to_projects' | 'changed_to_projects' | 'new_to_favorites' | 'changed_to_favorites'; + offer: OfferSummaryRecord; + relevantProjects?: PlanSummary[]; + notificationPriority: 'high' | 'medium' | 'low'; +} + +export interface TimeSafariProjectNotification { + type: 'project'; + subtype: 'local_and_new' | 'local_and_changed' | 'with_content_and_new' | 'favorite_and_changed'; + project: PlanSummary; + changes?: { + fields: string[]; + previousValues?: Record; + }; + relevantOffers?: OfferSummaryRecord[]; + notificationPriority: 'high' | 'medium' | 'low'; +} + +export interface TimeSafariPersonNotification { + type: 'person'; + subtype: 'local_and_new' | 'local_and_changed' | 'with_content_and_new' | 'favorite_and_changed'; + personDid: string; + changes?: { + fields: string[]; + previousValues?: Record; + }; + relevantProjects?: PlanSummary[]; + notificationPriority: 'high' | 'medium' | 'low'; +} + +export interface TimeSafariItemNotification { + type: 'item'; + subtype: 'local_and_new' | 'local_and_changed' | 'favorite_and_changed'; + itemId: string; + changes?: { + fields: string[]; + previousValues?: Record; + }; + relevantContext?: 'project' | 'offer' | 'person'; + notificationPriority: 'high' | 'medium' | 'low'; +} + +// Union type for TimeSafari notifications +export type TimeSafariNotification = + | TimeSafariOfferNotification + | TimeSafariProjectNotification + | TimeSafariPersonNotification + | TimeSafariItemNotification; + +// Enhanced ActiveDid Management Events +export interface ActiveDidChangeEventEnhanced extends ActiveDidChangeEvent { + sourceComponent: string; // 'host' | 'plugin' | 'background' | 'sync' + changeReason: 'user_switch' | 'session_expired' | 'background_refresh' | 'setup'; + transitionDurationMs?: number; + relatedNotifications?: TimeSafariNotification[]; +} + +// TimeSafari-specific Platform Configuration +export interface TimeSafariPlatformConfig { + platform: 'android' | 'ios' | 'web' | 'electron'; + storageType: 'plugin-managed' | 'host-managed'; + syncStrategy: 'immediate' | 'batched' | 'scheduled'; + permissions: { + notifications: boolean; + backgroundRefresh: boolean; + networkAccess: boolean; + }; + capabilities: { + pushNotifications: boolean; + backgroundTasks: boolean; + identityManagement: boolean; + cryptoSigning: boolean; + }; +} + export interface ActiveDidIntegrationConfig { platform: 'android' | 'ios' | 'web' | 'electron'; storageType: 'plugin-managed' | 'host-managed';