From fd4ddcbd60ba40771096fde12f9b99b689f639b0 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Wed, 29 Oct 2025 11:52:15 +0000 Subject: [PATCH] feat(android): add runtime starred plans management API - Add updateStarredPlans() method to update plan IDs from TimeSafari app - Stores plan IDs in SharedPreferences for persistence - Integrated with TimeSafariIntegrationManager for prefetch operations - Includes comprehensive logging for debugging - Add getStarredPlans() method to retrieve current stored plan IDs - Allows TimeSafari app to verify synchronization - Returns count and last update timestamp - Update TimeSafariIntegrationManager to load starred plan IDs - Reads from SharedPreferences when building TimeSafariUserConfig - Used automatically by EnhancedDailyNotificationFetcher for API calls - Enables dynamic updates without requiring app restart - Add TypeScript definitions for new methods - Includes JSDoc documentation for integration guidance - Matches Android implementation return types - Create integration example for TimeSafari app - Shows how to sync plan IDs from account settings - Demonstrates star/unstar action handling - Includes verification and error handling patterns This allows the TimeSafari app to dynamically update starred project IDs when users star or unstar projects, without requiring plugin configuration changes or app restarts. The stored IDs are automatically used by the prefetch system to query for project updates. --- .../DailyNotificationPlugin.java | 120 ++++++++ .../TimeSafariIntegrationManager.java | 52 ++++ .../timesafari-starred-plans-integration.ts | 269 ++++++++++++++++++ src/definitions.ts | 32 +++ 4 files changed, 473 insertions(+) create mode 100644 examples/timesafari-starred-plans-integration.ts diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.java b/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.java index ae1d429..a9bac91 100644 --- a/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.java +++ b/android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.java @@ -1786,6 +1786,126 @@ public class DailyNotificationPlugin extends Plugin { } } + /** + * Update starred plan IDs from host application + * + * This allows the TimeSafari app to dynamically update the list of starred + * project IDs when users star or unstar projects. The IDs are stored persistently + * and used for prefetch operations that query for starred project updates. + * + * @param call Contains: + * - planIds: string[] - Array of starred plan handle IDs + */ + @PluginMethod + public void updateStarredPlans(PluginCall call) { + try { + JSObject data = call.getData(); + if (data == null) { + call.reject("No data provided"); + return; + } + + Object planIdsObj = data.get("planIds"); + if (planIdsObj == null) { + call.reject("planIds is required"); + return; + } + + // Convert to List + List planIds; + if (planIdsObj instanceof List) { + @SuppressWarnings("unchecked") + List objList = (List) planIdsObj; + planIds = new java.util.ArrayList<>(); + for (Object obj : objList) { + if (obj != null) { + planIds.add(obj.toString()); + } + } + } else { + call.reject("planIds must be an array"); + return; + } + + Log.i(TAG, "DN|UPDATE_STARRED_PLANS count=" + planIds.size()); + + // Store in SharedPreferences for persistence + SharedPreferences preferences = getContext() + .getSharedPreferences("daily_notification_timesafari", Context.MODE_PRIVATE); + + // Store as JSON string for easy retrieval + org.json.JSONArray jsonArray = new org.json.JSONArray(); + for (String planId : planIds) { + jsonArray.put(planId); + } + + preferences.edit() + .putString("starredPlanIds", jsonArray.toString()) + .putLong("starredPlansUpdatedAt", System.currentTimeMillis()) + .apply(); + + Log.d(TAG, "DN|STARRED_PLANS_STORED count=" + planIds.size() + + " stored_at=" + System.currentTimeMillis()); + + // Update TimeSafariIntegrationManager if it needs the IDs immediately + if (timeSafariIntegration != null) { + // The TimeSafariIntegrationManager will read from SharedPreferences + // when it needs the starred plan IDs, so no direct update needed + Log.d(TAG, "DN|STARRED_PLANS_UPDATED TimeSafariIntegrationManager will use stored IDs"); + } + + JSObject result = new JSObject(); + result.put("success", true); + result.put("planIdsCount", planIds.size()); + result.put("updatedAt", System.currentTimeMillis()); + + call.resolve(result); + + } catch (Exception e) { + Log.e(TAG, "DN|UPDATE_STARRED_PLANS_ERR Error updating starred plans", e); + call.reject("Error updating starred plans: " + e.getMessage()); + } + } + + /** + * Get current starred plan IDs + * + * Returns the currently stored starred plan IDs from SharedPreferences. + * This is useful for the host app to verify what IDs are stored. + */ + @PluginMethod + public void getStarredPlans(PluginCall call) { + try { + SharedPreferences preferences = getContext() + .getSharedPreferences("daily_notification_timesafari", Context.MODE_PRIVATE); + + String starredPlansJson = preferences.getString("starredPlanIds", "[]"); + long updatedAt = preferences.getLong("starredPlansUpdatedAt", 0); + + org.json.JSONArray jsonArray = new org.json.JSONArray(starredPlansJson); + List planIds = new java.util.ArrayList<>(); + for (int i = 0; i < jsonArray.length(); i++) { + planIds.add(jsonArray.getString(i)); + } + + JSObject result = new JSObject(); + org.json.JSONArray planIdsArray = new org.json.JSONArray(); + for (String planId : planIds) { + planIdsArray.put(planId); + } + result.put("planIds", planIdsArray); + result.put("count", planIds.size()); + result.put("updatedAt", updatedAt); + + Log.d(TAG, "DN|GET_STARRED_PLANS count=" + planIds.size()); + call.resolve(result); + + } catch (Exception e) { + Log.e(TAG, "DN|GET_STARRED_PLANS_ERR Error getting starred plans", e); + call.reject("Error getting starred plans: " + e.getMessage()); + } + } + /** * Test JWT generation for debugging */ diff --git a/android/plugin/src/main/java/com/timesafari/dailynotification/TimeSafariIntegrationManager.java b/android/plugin/src/main/java/com/timesafari/dailynotification/TimeSafariIntegrationManager.java index e93d704..dd6d67d 100644 --- a/android/plugin/src/main/java/com/timesafari/dailynotification/TimeSafariIntegrationManager.java +++ b/android/plugin/src/main/java/com/timesafari/dailynotification/TimeSafariIntegrationManager.java @@ -27,18 +27,23 @@ package com.timesafari.dailynotification; import android.content.Context; +import android.content.SharedPreferences; import android.util.Log; import androidx.annotation.MainThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.concurrent.Executors; +import org.json.JSONArray; +import org.json.JSONException; + /** * TimeSafari Integration Manager * @@ -272,6 +277,11 @@ public final class TimeSafariIntegrationManager { userConfig.fetchOffersToProjects = true; userConfig.fetchProjectUpdates = true; + // Load starred plan IDs from SharedPreferences + userConfig.starredPlanIds = loadStarredPlanIdsFromSharedPreferences(); + logger.d("TS: Loaded starredPlanIds count=" + + (userConfig.starredPlanIds != null ? userConfig.starredPlanIds.size() : 0)); + // 3) Execute fetch (async, but we wait in executor) CompletableFuture future = fetcher.fetchAllTimeSafariData(userConfig); @@ -584,5 +594,47 @@ public final class TimeSafariIntegrationManager { // If you replace the Executor with something closeable, do it here // For now, single-threaded executor will be GC'd when manager is GC'd } + + /* ============================================================ + * Helper Methods + * ============================================================ */ + + /** + * Load starred plan IDs from SharedPreferences + * + * Reads the persisted starred plan IDs that were stored via + * DailyNotificationPlugin.updateStarredPlans() + * + * @return List of starred plan IDs, or empty list if none stored + */ + @NonNull + private List loadStarredPlanIdsFromSharedPreferences() { + try { + SharedPreferences preferences = appContext + .getSharedPreferences("daily_notification_timesafari", Context.MODE_PRIVATE); + + String starredPlansJson = preferences.getString("starredPlanIds", "[]"); + + if (starredPlansJson == null || starredPlansJson.isEmpty()) { + return new ArrayList<>(); + } + + JSONArray jsonArray = new JSONArray(starredPlansJson); + List planIds = new ArrayList<>(); + + for (int i = 0; i < jsonArray.length(); i++) { + planIds.add(jsonArray.getString(i)); + } + + return planIds; + + } catch (JSONException e) { + logger.e("TS: Error parsing starredPlanIds from SharedPreferences", e); + return new ArrayList<>(); + } catch (Exception e) { + logger.e("TS: Unexpected error loading starredPlanIds", e); + return new ArrayList<>(); + } + } } diff --git a/examples/timesafari-starred-plans-integration.ts b/examples/timesafari-starred-plans-integration.ts new file mode 100644 index 0000000..bcaf077 --- /dev/null +++ b/examples/timesafari-starred-plans-integration.ts @@ -0,0 +1,269 @@ +/** + * TimeSafari Starred Plans Integration Example + * + * Demonstrates how to integrate the Daily Notification Plugin's starred plans + * management with the TimeSafari app's starred projects functionality. + * + * This example shows: + * 1. How to update starred plan IDs when users star/unstar projects + * 2. How to sync starred plans on app startup + * 3. How to verify stored plan IDs + * + * @author Matthew Raymer + * @version 1.0.0 + */ + +import { DailyNotification } from '@timesafari/daily-notification-plugin'; +import { logger } from './logger'; // Assuming a logger utility + +/** + * TimeSafari Starred Plans Manager + * + * Integrates with the Daily Notification Plugin to keep starred plan IDs + * synchronized with the TimeSafari app's account settings. + */ +export class TimeSafariStarredPlansManager { + private plugin: DailyNotification; + private currentPlanIds: string[] = []; + + constructor(plugin: DailyNotification) { + this.plugin = plugin; + } + + /** + * Sync starred plans from account settings to the plugin + * + * Call this when: + * - App starts up + * - User logs in + * - Account settings are refreshed + * + * @param starredPlanHandleIds Array of plan handle IDs from account settings + */ + async syncStarredPlansFromAccount( + starredPlanHandleIds: string[] + ): Promise { + try { + logger.info('Syncing starred plans to plugin', { + count: starredPlanHandleIds.length + }); + + // Update plugin with current starred plan IDs + const result = await this.plugin.updateStarredPlans({ + planIds: starredPlanHandleIds + }); + + if (result.success) { + this.currentPlanIds = starredPlanHandleIds; + logger.info('Starred plans synced successfully', { + count: result.planIdsCount, + updatedAt: new Date(result.updatedAt).toISOString() + }); + } else { + logger.error('Failed to sync starred plans to plugin'); + } + } catch (error) { + logger.error('Error syncing starred plans', error); + throw error; + } + } + + /** + * Update starred plans when a user stars a project + * + * Call this when: + * - User clicks star on a project + * - Star action completes successfully + * + * @param planHandleId The plan handle ID that was starred + */ + async addStarredPlan(planHandleId: string): Promise { + try { + // Get current starred plans from plugin (to avoid duplicate updates) + const current = await this.plugin.getStarredPlans(); + + // Add new plan ID if not already present + if (!current.planIds.includes(planHandleId)) { + const updatedPlanIds = [...current.planIds, planHandleId]; + + await this.plugin.updateStarredPlans({ + planIds: updatedPlanIds + }); + + this.currentPlanIds = updatedPlanIds; + + logger.info('Starred plan added', { planHandleId }); + } else { + logger.debug('Plan already starred', { planHandleId }); + } + } catch (error) { + logger.error('Error adding starred plan', error); + throw error; + } + } + + /** + * Update starred plans when a user unstars a project + * + * Call this when: + * - User clicks unstar on a project + * - Unstar action completes successfully + * + * @param planHandleId The plan handle ID that was unstarred + */ + async removeStarredPlan(planHandleId: string): Promise { + try { + // Get current starred plans from plugin + const current = await this.plugin.getStarredPlans(); + + // Remove plan ID if present + const updatedPlanIds = current.planIds.filter( + id => id !== planHandleId + ); + + if (updatedPlanIds.length !== current.planIds.length) { + await this.plugin.updateStarredPlans({ + planIds: updatedPlanIds + }); + + this.currentPlanIds = updatedPlanIds; + + logger.info('Starred plan removed', { planHandleId }); + } else { + logger.debug('Plan not in starred list', { planHandleId }); + } + } catch (error) { + logger.error('Error removing starred plan', error); + throw error; + } + } + + /** + * Get current starred plans from plugin + * + * Useful for verifying synchronization or displaying current state. + * + * @returns Current starred plan IDs stored in the plugin + */ + async getCurrentStarredPlans(): Promise { + try { + const result = await this.plugin.getStarredPlans(); + this.currentPlanIds = result.planIds; + return result.planIds; + } catch (error) { + logger.error('Error getting starred plans', error); + throw error; + } + } + + /** + * Verify starred plans synchronization + * + * Compares account settings with plugin storage to ensure they match. + * Useful for debugging or validation after sync operations. + * + * @param accountPlanIds Plan IDs from account settings + * @returns Object with sync status and any mismatches + */ + async verifySync(accountPlanIds: string[]): Promise<{ + inSync: boolean; + accountCount: number; + pluginCount: number; + mismatches: { + inAccountNotPlugin: string[]; + inPluginNotAccount: string[]; + }; + }> { + try { + const pluginResult = await this.plugin.getStarredPlans(); + const pluginPlanIds = pluginResult.planIds; + + const inAccountNotPlugin = accountPlanIds.filter( + id => !pluginPlanIds.includes(id) + ); + const inPluginNotAccount = pluginPlanIds.filter( + id => !accountPlanIds.includes(id) + ); + + const inSync = + inAccountNotPlugin.length === 0 && inPluginNotAccount.length === 0; + + return { + inSync, + accountCount: accountPlanIds.length, + pluginCount: pluginPlanIds.length, + mismatches: { + inAccountNotPlugin, + inPluginNotAccount + } + }; + } catch (error) { + logger.error('Error verifying sync', error); + throw error; + } + } +} + +/** + * Example integration with TimeSafari's searchStarred method + * + * This shows how to integrate the starred plans manager with the existing + * TimeSafari searchStarred method. + */ +export async function integrateWithSearchStarred( + plugin: DailyNotification, + accountSettings: { starredPlanHandleIds?: string[] } +): Promise { + const manager = new TimeSafariStarredPlansManager(plugin); + + try { + // Get starred plan IDs from account settings + const starredIds = accountSettings.starredPlanHandleIds || []; + + if (starredIds.length === 0) { + logger.info('No starred plans to sync'); + return; + } + + // Sync to plugin + await manager.syncStarredPlansFromAccount(starredIds); + + // Verify sync (optional, for debugging) + const syncStatus = await manager.verifySync(starredIds); + if (!syncStatus.inSync) { + logger.warn('Starred plans sync verification failed', syncStatus); + // Optionally retry sync if verification fails + await manager.syncStarredPlansFromAccount(starredIds); + } else { + logger.info('Starred plans sync verified'); + } + } catch (error) { + logger.error('Error integrating with searchStarred', error); + // Don't throw - allow searchStarred to continue even if plugin sync fails + } +} + +/** + * Example: Hook into star/unstar actions + * + * This shows how to update the plugin when users star or unstar projects. + */ +export async function handleStarAction( + plugin: DailyNotification, + planHandleId: string, + isStarring: boolean +): Promise { + const manager = new TimeSafariStarredPlansManager(plugin); + + try { + if (isStarring) { + await manager.addStarredPlan(planHandleId); + } else { + await manager.removeStarredPlan(planHandleId); + } + } catch (error) { + logger.error('Error handling star action', error); + // Don't throw - allow star action to complete even if plugin update fails + } +} + diff --git a/src/definitions.ts b/src/definitions.ts index 7803352..17a1122 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -374,6 +374,38 @@ export interface DailyNotificationPlugin { clearCacheForNewIdentity(): Promise; updateBackgroundTaskIdentity(activeDid: string): Promise; + // Starred Plans Management Methods + /** + * Update starred plan IDs from host application + * + * This allows the TimeSafari app to dynamically update the list of starred + * project IDs when users star or unstar projects. The IDs are stored persistently + * and used for prefetch operations that query for starred project updates. + * + * @param options Contains: + * - planIds: string[] - Array of starred plan handle IDs + * @returns Promise with success status and plan count + */ + updateStarredPlans(options: { planIds: string[] }): Promise<{ + success: boolean; + planIdsCount: number; + updatedAt: number; + }>; + + /** + * Get current starred plan IDs + * + * Returns the currently stored starred plan IDs from SharedPreferences. + * This is useful for the host app to verify what IDs are stored. + * + * @returns Promise with current starred plan IDs + */ + getStarredPlans(): Promise<{ + planIds: string[]; + count: number; + updatedAt: number; + }>; + // Content Fetching Methods /** * Trigger an immediate standalone fetch for content updates