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