Browse Source

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.
master
Matthew Raymer 3 days ago
parent
commit
fd4ddcbd60
  1. 120
      android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.java
  2. 52
      android/plugin/src/main/java/com/timesafari/dailynotification/TimeSafariIntegrationManager.java
  3. 269
      examples/timesafari-starred-plans-integration.ts
  4. 32
      src/definitions.ts

120
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<String>
List<String> planIds;
if (planIdsObj instanceof List) {
@SuppressWarnings("unchecked")
List<Object> objList = (List<Object>) 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<String> 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 * Test JWT generation for debugging
*/ */

52
android/plugin/src/main/java/com/timesafari/dailynotification/TimeSafariIntegrationManager.java

@ -27,18 +27,23 @@
package com.timesafari.dailynotification; package com.timesafari.dailynotification;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log; import android.util.Log;
import androidx.annotation.MainThread; import androidx.annotation.MainThread;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import org.json.JSONArray;
import org.json.JSONException;
/** /**
* TimeSafari Integration Manager * TimeSafari Integration Manager
* *
@ -272,6 +277,11 @@ public final class TimeSafariIntegrationManager {
userConfig.fetchOffersToProjects = true; userConfig.fetchOffersToProjects = true;
userConfig.fetchProjectUpdates = 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) // 3) Execute fetch (async, but we wait in executor)
CompletableFuture<EnhancedDailyNotificationFetcher.TimeSafariNotificationBundle> future = CompletableFuture<EnhancedDailyNotificationFetcher.TimeSafariNotificationBundle> future =
fetcher.fetchAllTimeSafariData(userConfig); fetcher.fetchAllTimeSafariData(userConfig);
@ -584,5 +594,47 @@ public final class TimeSafariIntegrationManager {
// If you replace the Executor with something closeable, do it here // 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 // 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<String> 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<String> 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<>();
}
}
} }

269
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<void> {
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<void> {
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<void> {
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<string[]> {
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<void> {
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<void> {
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
}
}

32
src/definitions.ts

@ -374,6 +374,38 @@ export interface DailyNotificationPlugin {
clearCacheForNewIdentity(): Promise<void>; clearCacheForNewIdentity(): Promise<void>;
updateBackgroundTaskIdentity(activeDid: string): Promise<void>; updateBackgroundTaskIdentity(activeDid: string): Promise<void>;
// 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 // Content Fetching Methods
/** /**
* Trigger an immediate standalone fetch for content updates * Trigger an immediate standalone fetch for content updates

Loading…
Cancel
Save