Files
daily-notification-plugin/src/definitions.ts
Matthew Raymer 73301f7d1d feat(android): add getSchedulesWithStatus() and alarm list UI
Adds ability to list alarms with AlarmManager status in web interface.

Changes:
- Add getSchedulesWithStatus() method to DailyNotificationPlugin
- Add ScheduleWithStatus TypeScript interface with isActuallyScheduled flag
- Add alarm list UI to android-test-app with status indicators

Backend:
- getSchedulesWithStatus() returns schedules with AlarmManager verification
- Checks isAlarmScheduled() for each notify schedule with nextRunAt
- Returns isActuallyScheduled boolean flag for each schedule

TypeScript:
- New ScheduleWithStatus interface extending Schedule
- Method signature with JSDoc and usage examples

UI:
- New "📋 List Alarms" button in test app
- Color-coded alarm cards (green=scheduled, orange=not scheduled)
- Shows schedule ID, next run time, pattern, and AlarmManager status
- Useful for debugging recovery scenarios and verifying alarm state

Use case:
- Verify which alarms are in database vs actually scheduled
- Debug Phase 1/2/3 recovery scenarios
- Visual confirmation of alarm state after app launch/boot

Related:
- Enhances: android-test-app for Phase 1-3 testing
- Supports: Recovery verification and debugging
2025-11-28 04:56:19 +00:00

1539 lines
45 KiB
TypeScript

/**
* Daily Notification Plugin Definitions
*
* TypeScript definitions for the Daily Notification Plugin
* Aligned with Android implementation and test requirements
*
* @author Matthew Raymer
* @version 2.0.0
*/
// Import SPI types from content-fetcher.ts
import type {
SchedulingPolicy,
JsNotificationContentFetcher
} from './types/content-fetcher';
export interface NotificationResponse {
id: string;
title: string;
body: string;
timestamp: number;
url?: string;
}
export interface NotificationOptions {
url?: string;
time?: string;
title?: string;
body?: string;
sound?: boolean;
priority?: 'high' | 'default' | 'low' | 'min' | 'max' | 'normal';
timezone?: string;
retryCount?: number;
retryInterval?: number;
offlineFallback?: boolean;
headers?: Record<string, string>;
}
export interface NotificationSettings {
url?: string;
time?: string;
sound?: boolean;
priority?: string;
timezone?: string;
retryCount?: number;
retryInterval?: number;
offlineFallback?: boolean;
}
export interface NotificationStatus {
isEnabled?: boolean;
isScheduled?: boolean;
lastNotificationTime: number | Promise<number>;
nextNotificationTime: number | Promise<number>;
pending?: number;
settings: NotificationSettings;
error?: string;
}
export interface BatteryStatus {
level: number;
isCharging: boolean;
powerState: number;
isOptimizationExempt: boolean;
}
export interface PowerState {
powerState: number;
isOptimizationExempt: boolean;
}
export interface NotificationEvent extends Event {
detail: {
id: string;
action: string;
data?: Record<string, unknown>;
};
}
export interface PermissionStatus {
status?: string;
granted?: boolean;
notifications: PermissionState;
backgroundRefresh?: PermissionState; // iOS only
alert?: boolean;
badge?: boolean;
sound?: boolean;
lockScreen?: boolean;
carPlay?: boolean;
}
/**
* Permission status result for checkPermissionStatus()
* Returns boolean flags for each permission type
*/
export interface PermissionStatusResult {
notificationsEnabled: boolean;
exactAlarmEnabled: boolean;
wakeLockEnabled: boolean;
allPermissionsGranted: boolean;
}
// Static Daily Reminder Interfaces
export interface DailyReminderOptions {
id: string;
title: string;
body: string;
time: string; // HH:mm format (e.g., "09:00")
sound?: boolean;
vibration?: boolean;
priority?: 'low' | 'normal' | 'high';
repeatDaily?: boolean;
timezone?: string;
}
export interface DailyReminderInfo {
id: string;
title: string;
body: string;
time: string;
sound: boolean;
vibration: boolean;
priority: 'low' | 'normal' | 'high';
repeatDaily: boolean;
timezone?: string;
isScheduled: boolean;
nextTriggerTime?: number;
createdAt: number;
lastTriggered?: number;
}
export type PermissionState = 'prompt' | 'prompt-with-rationale' | 'granted' | 'denied' | 'provisional' | 'ephemeral' | 'unknown';
// Additional interfaces for enhanced functionality
export interface NotificationMetrics {
scheduledTime: number;
actualDeliveryTime?: number;
contentAge: number;
engagement?: 'TAPPED' | 'DISMISSED' | 'IGNORED';
failureReason?: string;
platformInfo: PlatformInfo;
}
export interface PlatformInfo {
oem?: string;
osVersion: string;
appState: string;
}
export interface FallbackContent {
title: string;
body: string;
isEmergency: boolean;
age?: string;
}
export interface CachePolicy {
maxSize: number;
evictionPolicy: 'LRU' | 'FIFO' | 'TTL';
ttl?: number;
cleanupInterval: number;
}
export interface NetworkConfig {
timeout: number;
retryAttempts: number;
retryDelay: number;
offlineFallback: boolean;
}
export interface SchedulingConfig {
exactAlarms: boolean;
adaptiveScheduling: boolean;
quietHours?: {
start: string;
end: string;
enabled: boolean;
};
timezone: string;
}
export interface ConfigureOptions {
dbPath?: string;
storage?: 'shared' | 'tiered';
ttlSeconds?: number;
prefetchLeadMinutes?: number;
maxNotificationsPerDay?: number;
retentionDays?: number;
// Phase 2: TimeSafari ActiveDid Integration Enhancement
activeDidIntegration?: {
platform: 'android' | 'ios' | '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
};
}
// Dual Scheduling System Interfaces
export interface ContentFetchConfig {
enabled: boolean;
schedule: string; // Cron expression
url?: string;
headers?: Record<string, string>;
timeout?: number;
retryAttempts?: number;
retryDelay?: number;
callbacks: {
apiService?: string;
database?: string;
reporting?: string;
onSuccess?: (data: Record<string, unknown>) => Promise<void>;
onError?: (error: Error) => Promise<void>;
onComplete?: (result: ContentFetchResult) => Promise<void>;
};
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 {
enabled: boolean;
schedule: string; // Cron expression
title?: string;
body?: string;
sound?: boolean;
vibration?: boolean;
priority?: 'low' | 'normal' | 'high';
badge?: boolean;
actions?: NotificationAction[];
category?: string;
userInfo?: Record<string, unknown>;
}
export interface NotificationAction {
id: string;
title: string;
icon?: string;
destructive?: boolean;
authenticationRequired?: boolean;
}
export interface DualScheduleConfiguration {
contentFetch: ContentFetchConfig;
userNotification: UserNotificationConfig;
relationship?: {
autoLink: boolean; // Automatically link content to notification
contentTimeout: number; // How long to wait for content before notification
fallbackBehavior: 'skip' | 'show_default' | 'retry';
};
}
export interface ContentFetchResult {
success: boolean;
data?: Record<string, unknown>;
timestamp: number;
contentAge: number;
error?: string;
retryCount: number;
metadata?: Record<string, unknown>;
}
// ============================================================================
// DATABASE TYPE DEFINITIONS
// ============================================================================
// These types represent the plugin's internal SQLite database schema.
// The plugin owns its database, and these types are used for TypeScript
// access through Capacitor interfaces.
//
// See: docs/DATABASE_INTERFACES.md for complete documentation
// ============================================================================
/**
* Recurring schedule pattern stored in database
* Used to restore schedules after device reboot
*/
export interface Schedule {
/** Unique schedule identifier */
id: string;
/** Schedule type: 'fetch' for content fetching, 'notify' for notifications */
kind: 'fetch' | 'notify';
/** Cron expression (e.g., "0 9 * * *" for daily at 9 AM) */
cron?: string;
/** Clock time in HH:mm format (e.g., "09:00") */
clockTime?: string;
/** Whether schedule is enabled */
enabled: boolean;
/** Timestamp of last execution (milliseconds since epoch) */
lastRunAt?: number;
/** Timestamp of next scheduled execution (milliseconds since epoch) */
nextRunAt?: number;
/** Random jitter in milliseconds for timing variation */
jitterMs: number;
/** Backoff policy ('exp' for exponential, etc.) */
backoffPolicy: string;
/** Optional JSON state for advanced scheduling */
stateJson?: string;
}
/**
* Schedule with AlarmManager status
* Extends Schedule with isActuallyScheduled flag indicating if alarm is registered in AlarmManager
*/
export interface ScheduleWithStatus extends Schedule {
/** Whether the alarm is actually scheduled in AlarmManager (Android only, for 'notify' schedules) */
isActuallyScheduled: boolean;
}
/**
* Input type for creating a new schedule
*/
export interface CreateScheduleInput {
kind: 'fetch' | 'notify';
cron?: string;
clockTime?: string;
enabled?: boolean;
jitterMs?: number;
backoffPolicy?: string;
stateJson?: string;
}
/**
* Content cache entry with TTL
* Stores prefetched content for offline-first display
*/
export interface ContentCache {
/** Unique cache identifier */
id: string;
/** Timestamp when content was fetched (milliseconds since epoch) */
fetchedAt: number;
/** Time-to-live in seconds */
ttlSeconds: number;
/** Content payload (JSON string or base64 encoded) */
payload: string;
/** Optional metadata */
meta?: string;
}
/**
* Input type for creating a content cache entry
*/
export interface CreateContentCacheInput {
id?: string; // Auto-generated if not provided
payload: string;
ttlSeconds: number;
meta?: string;
}
/**
* Plugin configuration entry
* Stores user preferences and plugin settings
*/
export interface Config {
/** Unique configuration identifier */
id: string;
/** Optional TimeSafari DID for user-specific configs */
timesafariDid?: string;
/** Configuration type (e.g., 'plugin_setting', 'user_preference') */
configType: string;
/** Configuration key */
configKey: string;
/** Configuration value (stored as string, parsed based on configDataType) */
configValue: string;
/** Data type: 'string' | 'boolean' | 'integer' | 'long' | 'float' | 'double' | 'json' */
configDataType: string;
/** Whether value is encrypted */
isEncrypted: boolean;
/** Timestamp when config was created (milliseconds since epoch) */
createdAt: number;
/** Timestamp when config was last updated (milliseconds since epoch) */
updatedAt: number;
}
/**
* Input type for creating a configuration entry
*/
export interface CreateConfigInput {
id?: string; // Auto-generated if not provided
timesafariDid?: string;
configType: string;
configKey: string;
configValue: string;
configDataType?: string; // Defaults to 'string' if not provided
isEncrypted?: boolean;
}
/**
* Callback configuration
* Stores callback endpoint configurations for execution after events
*/
export interface Callback {
/** Unique callback identifier */
id: string;
/** Callback type: 'http' for HTTP requests, 'local' for local handlers, 'queue' for queue */
kind: 'http' | 'local' | 'queue';
/** Target URL or identifier */
target: string;
/** Optional JSON headers for HTTP callbacks */
headersJson?: string;
/** Whether callback is enabled */
enabled: boolean;
/** Timestamp when callback was created (milliseconds since epoch) */
createdAt: number;
}
/**
* Input type for creating a callback configuration
*/
export interface CreateCallbackInput {
id: string;
kind: 'http' | 'local' | 'queue';
target: string;
headersJson?: string;
enabled?: boolean;
}
/**
* Execution history entry
* Logs fetch/notify/callback execution for debugging and analytics
*/
export interface History {
/** Auto-incrementing history ID */
id: number;
/** Reference ID (content ID, schedule ID, etc.) */
refId: string;
/** Execution kind: 'fetch' | 'notify' | 'callback' | 'boot_recovery' */
kind: 'fetch' | 'notify' | 'callback' | 'boot_recovery';
/** Timestamp when execution occurred (milliseconds since epoch) */
occurredAt: number;
/** Execution duration in milliseconds */
durationMs?: number;
/** Outcome: 'success' | 'failure' | 'skipped_ttl' | 'circuit_open' */
outcome: string;
/** Optional JSON diagnostics */
diagJson?: string;
}
/**
* History statistics
*/
export interface HistoryStats {
/** Total number of history entries */
totalCount: number;
/** Count by outcome */
outcomes: Record<string, number>;
/** Count by kind */
kinds: Record<string, number>;
/** Most recent execution timestamp */
mostRecent?: number;
/** Oldest execution timestamp */
oldest?: number;
}
export interface DualScheduleStatus {
contentFetch: {
isEnabled: boolean;
isScheduled: boolean;
lastFetchTime?: number;
nextFetchTime?: number;
lastFetchResult?: ContentFetchResult;
pendingFetches: number;
};
userNotification: {
isEnabled: boolean;
isScheduled: boolean;
lastNotificationTime?: number;
nextNotificationTime?: number;
pendingNotifications: number;
};
relationship: {
isLinked: boolean;
contentAvailable: boolean;
lastLinkTime?: number;
};
overall: {
isActive: boolean;
lastActivity: number;
errorCount: number;
successRate: number;
};
}
// Enhanced DailyNotificationPlugin interface with dual scheduling
export interface DailyNotificationPlugin {
// Configuration methods
configure(options: ConfigureOptions): Promise<void>;
/**
* Configure native fetcher with API credentials (cross-platform)
*
* This method provides a cross-platform mechanism for passing API credentials
* from TypeScript/JavaScript code to native fetcher implementations. The
* configuration is passed directly without using platform-specific storage
* mechanisms, keeping the interface consistent across Android, iOS, and web.
*
* **Why this exists:**
* - Native fetchers run in background workers (WorkManager/BGTaskScheduler)
* - Background workers cannot access JavaScript variables or Capacitor bridge
* - This method provides direct injection without storage dependencies
*
* **When to call:**
* - After app startup, once API credentials are available
* - After user login/authentication, when activeDid changes
* - When API server URL changes (dev/staging/production)
*
* **Prerequisites:**
* - Native fetcher must be registered in `Application.onCreate()` (Android)
* or `AppDelegate.didFinishLaunching()` (iOS) BEFORE calling this method
* - Should be called before any background fetches occur
*
* **Thread Safety:**
* - Safe to call from any thread (main thread or background)
* - Configuration changes take effect immediately for subsequent fetches
* - Native implementations should use `volatile` fields for thread safety
เร็ *
* **Example:**
* ```typescript
* import { DailyNotification } from '@capacitor-community/daily-notification';
*
* await DailyNotification.configureNativeFetcher({
* apiBaseUrl: 'http://10.0.2.2:3000', // Android emulator → host localhost
* activeDid: 'did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F',
* jwtSecret: 'test-jwt-secret-for-development'
* });
* ```
*
* **Error Handling:**
* - Rejects if required parameters are missing
* - Rejects if no native fetcher is registered
* - Rejects if native fetcher's `configure()` throws exception
*
* @param options Configuration options:
* - `apiBaseUrl` (required): Base URL for API server.
* - Android emulator: `"http://10.0.2.2:3000"` (maps to host localhost:3000)
* - iOS simulator: `"http://localhost:3000"`
* - Production: `"https://api.timesafari.com"`
* - `activeDid` (required): Active DID for authentication.
* Format: `"did:ethr:0x..."`. Used as JWT issuer/subject.
* - `jwtToken` (required): Pre-generated JWT token (ES256K signed).
* Generated in TypeScript using TimeSafari's `createEndorserJwtForKey()` function.
* **Note**: Token should be ES256K signed (DID-based), not HS256.
*
* **Architecture Note**: JWT tokens should be generated in TypeScript using TimeSafari's
* `createEndorserJwtForKey()` function (which uses DID-based ES256K signing), then passed
* to this method. This avoids the complexity of implementing DID-based JWT signing in Java.
*
* @throws {Error} If configuration fails (missing params, no fetcher registered, etc.)
*
* @see {@link https://github.com/timesafari/daily-notification-plugin/blob/main/docs/NATIVE_FETCHER_CONFIGURATION.md | Native Fetcher Configuration Guide}
* for complete documentation and examples
*/
configureNativeFetcher(options: {
apiBaseUrl: string;
activeDid: string;
jwtToken: string; // Pre-generated JWT token (ES256K signed) from TypeScript
}): Promise<void>;
// Rolling window management
maintainRollingWindow(): Promise<void>;
getRollingWindowStats(): Promise<{
stats: string;
maintenanceNeeded: boolean;
timeUntilNextMaintenance: number;
}>;
// Exact alarm management
getExactAlarmStatus(): Promise<{
supported: boolean;
enabled: boolean;
canSchedule: boolean;
fallbackWindow: string;
}>;
requestExactAlarmPermission(): Promise<void>;
openExactAlarmSettings(): Promise<void>;
// Reboot recovery management
getRebootRecoveryStatus(): Promise<{
inProgress: boolean;
lastRecoveryTime: number;
timeSinceLastRecovery: number;
recoveryNeeded: boolean;
}>;
// Existing methods
scheduleDailyNotification(options: NotificationOptions): Promise<void>;
/**
* Check if an alarm is scheduled for a given trigger time
* @param options Object containing triggerAtMillis (number)
* @returns Object with scheduled (boolean) and triggerAtMillis (number)
*/
isAlarmScheduled(options: { triggerAtMillis: number }): Promise<{ scheduled: boolean; triggerAtMillis: number }>;
/**
* Get the next scheduled alarm time from AlarmManager
* @returns Object with scheduled (boolean) and triggerAtMillis (number | null)
*/
getNextAlarmTime(): Promise<{ scheduled: boolean; triggerAtMillis?: number }>;
/**
* Test method: Schedule an alarm to fire in a few seconds
* Useful for verifying alarm delivery works correctly
* @param options Object containing secondsFromNow (number, default: 5)
* @returns Object with scheduled (boolean), secondsFromNow (number), and triggerAtMillis (number)
*/
testAlarm(options?: { secondsFromNow?: number }): Promise<{ scheduled: boolean; secondsFromNow: number; triggerAtMillis: number }>;
getLastNotification(): Promise<NotificationResponse | null>;
cancelAllNotifications(): Promise<void>;
getNotificationStatus(): Promise<NotificationStatus>;
updateSettings(settings: NotificationSettings): Promise<void>;
getBatteryStatus(): Promise<BatteryStatus>;
requestBatteryOptimizationExemption(): Promise<void>;
setAdaptiveScheduling(options: { enabled: boolean }): Promise<void>;
getPowerState(): Promise<PowerState>;
checkPermissions(): Promise<PermissionStatus>;
requestPermissions(): Promise<PermissionStatus>;
checkPermissionStatus(): Promise<PermissionStatusResult>;
requestNotificationPermissions(): Promise<PermissionStatus>;
isChannelEnabled(channelId?: string): Promise<{ enabled: boolean; channelId: string }>;
openChannelSettings(channelId?: string): Promise<void>;
checkStatus(): Promise<NotificationStatus>;
// New dual scheduling methods
scheduleContentFetch(config: ContentFetchConfig): Promise<void>;
scheduleUserNotification(config: UserNotificationConfig): Promise<void>;
scheduleDualNotification(config: DualScheduleConfiguration): Promise<void>;
getDualScheduleStatus(): Promise<DualScheduleStatus>;
updateDualScheduleConfig(config: DualScheduleConfiguration): Promise<void>;
cancelDualSchedule(): Promise<void>;
pauseDualSchedule(): Promise<void>;
resumeDualSchedule(): Promise<void>;
// Content management methods
getContentCache(): Promise<Record<string, unknown>>;
clearContentCache(): Promise<void>;
getContentHistory(): Promise<ContentFetchResult[]>;
// Callback management methods
registerCallback(name: string, callback: (...args: unknown[]) => void): Promise<void>;
unregisterCallback(name: string): Promise<void>;
getRegisteredCallbacks(): Promise<string[]>;
// ============================================================================
// DATABASE ACCESS METHODS
// ============================================================================
// These methods provide TypeScript/JavaScript access to the plugin's internal
// SQLite database. Since the plugin owns its database, the host app/webview
// accesses data through these Capacitor interfaces.
//
// Usage Pattern:
// import { DailyNotification } from '@capacitor-community/daily-notification';
// const schedules = await DailyNotification.getSchedules({ kind: 'notify' });
//
// See: docs/DATABASE_INTERFACES.md for complete documentation
// ============================================================================
/**
* Get all schedules matching optional filters
*
* @param options Optional filters:
* - kind: Filter by schedule type ('fetch' | 'notify')
* - enabled: Filter by enabled status (true = only enabled, false = only disabled, undefined = all)
* @returns Promise resolving to object with schedules array: { schedules: Schedule[] }
*
* @example
* ```typescript
* // Get all enabled notification schedules
* const result = await DailyNotification.getSchedules({
* kind: 'notify',
* enabled: true
* });
* const schedules = result.schedules;
* ```
*/
getSchedules(options?: { kind?: 'fetch' | 'notify'; enabled?: boolean }): Promise<{ schedules: Schedule[] }>;
/**
* Get all schedules with their AlarmManager status
* Returns schedules from database with isActuallyScheduled flag for each
*
* @param options Optional filters:
* - kind: Filter by schedule type ('fetch' | 'notify')
* - enabled: Filter by enabled status (true = only enabled, false = only disabled, undefined = all)
* @returns Promise resolving to object with schedules array: { schedules: ScheduleWithStatus[] }
*
* @example
* ```typescript
* // Get all notification schedules with AlarmManager status
* const result = await DailyNotification.getSchedulesWithStatus({
* kind: 'notify',
* enabled: true
* });
* result.schedules.forEach(schedule => {
* console.log(`${schedule.id}: ${schedule.isActuallyScheduled ? 'Scheduled' : 'Not scheduled'}`);
* });
* ```
*/
getSchedulesWithStatus(options?: { kind?: 'fetch' | 'notify'; enabled?: boolean }): Promise<{ schedules: ScheduleWithStatus[] }>;
/**
* Get a single schedule by ID
*
* @param id Schedule ID
* @returns Promise resolving to Schedule object or null if not found
*/
getSchedule(id: string): Promise<Schedule | null>;
/**
* Create a new recurring schedule
*
* @param schedule Schedule configuration
* @returns Promise resolving to created Schedule object
*
* @example
* ```typescript
* const schedule = await DailyNotification.createSchedule({
* kind: 'notify',
* cron: '0 9 * * *', // Daily at 9 AM
* enabled: true
* });
* ```
*/
createSchedule(schedule: CreateScheduleInput): Promise<Schedule>;
/**
* Update an existing schedule
*
* @param id Schedule ID
* @param updates Partial schedule updates
* @returns Promise resolving to updated Schedule object
*/
updateSchedule(id: string, updates: Partial<Schedule>): Promise<Schedule>;
/**
* Delete a schedule
*
* @param id Schedule ID
* @returns Promise resolving when deletion completes
*/
deleteSchedule(id: string): Promise<void>;
/**
* Enable or disable a schedule
*
* @param id Schedule ID
* @param enabled Enable state
* @returns Promise resolving when update completes
*/
enableSchedule(id: string, enabled: boolean): Promise<void>;
/**
* Calculate next run time from a cron expression or clockTime
*
* @param schedule Cron expression (e.g., "0 9 * * *") or clockTime (e.g., "09:00")
* @returns Promise resolving to timestamp (milliseconds since epoch)
*/
calculateNextRunTime(schedule: string): Promise<number>;
/**
* Get content cache by ID or latest cache
*
* @param options Optional filters:
* - id: Specific cache ID (if not provided, returns latest)
* @returns Promise resolving to ContentCache object or null
*/
getContentCacheById(options?: { id?: string }): Promise<ContentCache | null>;
/**
* Get the latest content cache entry
*
* @returns Promise resolving to latest ContentCache object or null
*/
getLatestContentCache(): Promise<ContentCache | null>;
/**
* Get content cache history
*
* @param limit Maximum number of entries to return (default: 10)
* @returns Promise resolving to object with history array: { history: ContentCache[] }
*/
getContentCacheHistory(limit?: number): Promise<{ history: ContentCache[] }>;
/**
* Save content to cache
*
* @param content Content cache data
* @returns Promise resolving to saved ContentCache object
*
* @example
* ```typescript
* await DailyNotification.saveContentCache({
* id: 'cache_123',
* payload: JSON.stringify({ title: 'Hello', body: 'World' }),
* ttlSeconds: 3600,
* meta: 'fetched_from_api'
* });
* ```
*/
saveContentCache(content: CreateContentCacheInput): Promise<ContentCache>;
/**
* Clear content cache entries
*
* @param options Optional filters:
* - olderThan: Only clear entries older than this timestamp (milliseconds)
* @returns Promise resolving when cleanup completes
*/
clearContentCacheEntries(options?: { olderThan?: number }): Promise<void>;
/**
* Get configuration value
*
* @param key Configuration key
* @param options Optional filters:
* - timesafariDid: Filter by TimeSafari DID
* @returns Promise resolving to Config object or null
*/
getConfig(key: string, options?: { timesafariDid?: string }): Promise<Config | null>;
/**
* Get all configurations matching filters
*
* @param options Optional filters:
* - timesafariDid: Filter by TimeSafari DID
* - configType: Filter by configuration type
* @returns Promise resolving to array of Config objects
*/
getAllConfigs(options?: { timesafariDid?: string; configType?: string }): Promise<{ configs: Config[] }>;
/**
* Set configuration value
*
* @param config Configuration data
* @returns Promise resolving to saved Config object
*/
setConfig(config: CreateConfigInput): Promise<Config>;
/**
* Update configuration value
*
* @param key Configuration key
* @param value New value (will be stringified based on dataType)
* @param options Optional filters:
* - timesafariDid: Filter by TimeSafari DID
* @returns Promise resolving to updated Config object
*/
updateConfig(key: string, value: string, options?: { timesafariDid?: string }): Promise<Config>;
/**
* Delete configuration
*
* @param key Configuration key
* @param options Optional filters:
* - timesafariDid: Filter by TimeSafari DID
* @returns Promise resolving when deletion completes
*/
deleteConfig(key: string, options?: { timesafariDid?: string }): Promise<void>;
/**
* Get all callbacks matching filters
*
* @param options Optional filters:
* - enabled: Filter by enabled status
* @returns Promise resolving to object with callbacks array: { callbacks: Callback[] }
*/
getCallbacks(options?: { enabled?: boolean }): Promise<{ callbacks: Callback[] }>;
/**
* Get a single callback by ID
*
* @param id Callback ID
* @returns Promise resolving to Callback object or null
*/
getCallback(id: string): Promise<Callback | null>;
/**
* Register a new callback
*
* @param callback Callback configuration
* @returns Promise resolving to created Callback object
*/
registerCallbackConfig(callback: CreateCallbackInput): Promise<Callback>;
/**
* Update an existing callback
*
* @param id Callback ID
* @param updates Partial callback updates
* @returns Promise resolving to updated Callback object
*/
updateCallback(id: string, updates: Partial<Callback>): Promise<Callback>;
/**
* Delete a callback
*
* @param id Callback ID
* @returns Promise resolving when deletion completes
*/
deleteCallback(id: string): Promise<void>;
/**
* Enable or disable a callback
*
* @param id Callback ID
* @param enabled Enable state
* @returns Promise resolving when update completes
*/
enableCallback(id: string, enabled: boolean): Promise<void>;
/**
* Get execution history
*
* @param options Optional filters:
* - since: Only return entries after this timestamp (milliseconds)
* - kind: Filter by execution kind ('fetch' | 'notify' | 'callback')
* - limit: Maximum number of entries to return (default: 50)
* @returns Promise resolving to object with history array: { history: History[] }
*/
getHistory(options?: {
since?: number;
kind?: 'fetch' | 'notify' | 'callback';
limit?: number;
}): Promise<{ history: History[] }>;
/**
* Get history statistics
*
* @returns Promise resolving to history statistics
*/
getHistoryStats(): Promise<HistoryStats>;
// Phase 1: ActiveDid Management Methods (Option A Implementation)
setActiveDidFromHost(activeDid: string): Promise<void>;
onActiveDidChange(callback: (newActiveDid: string) => Promise<void>): void;
refreshAuthenticationForNewIdentity(activeDid: string): Promise<void>;
clearCacheForNewIdentity(): 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
/**
* Trigger an immediate standalone fetch for content updates
*
* This method allows manual triggering of content fetches independently of
* scheduled notifications. Useful for on-demand content refresh, cache warming,
* or background sync operations.
*
* @returns Promise with success status and message
*/
triggerImmediateFetch(): Promise<{
success: boolean;
message: string;
}>;
// Static Daily Reminder Methods
/**
* Schedule a simple daily reminder notification
* No network content required - just static text
*/
scheduleDailyReminder(options: DailyReminderOptions): Promise<void>;
/**
* Cancel a daily reminder notification
*/
cancelDailyReminder(reminderId: string): Promise<void>;
/**
* Get all scheduled daily reminders
*/
getScheduledReminders(): Promise<DailyReminderInfo[]>;
/**
* Update an existing daily reminder
*/
updateDailyReminder(reminderId: string, options: DailyReminderOptions): Promise<void>;
// Integration Point Refactor (PR1): SPI Registration Methods
/**
* Set JavaScript content fetcher for foreground operations
*
* NOTE: This is a stub in PR1. Full implementation coming in PR3.
* JS fetchers are ONLY used for foreground/manual refresh.
* Background workers must use native fetcher.
*
* @param fetcher JavaScript fetcher implementation
*/
setJsContentFetcher(fetcher: JsNotificationContentFetcher): void;
/**
* Enable or disable native fetcher
*
* Native fetcher is required for background workers. If disabled,
* background fetches will fail gracefully.
*
* @param enable Whether to enable native fetcher
* @returns Promise with enabled and registered status
*/
enableNativeFetcher(enable: boolean): Promise<{
enabled: boolean;
registered: boolean;
}>;
/**
* Set scheduling policy configuration
*
* Updates the scheduling policy used by the plugin for retry backoff,
* prefetch timing, deduplication, and cache TTL.
*
* @param policy Scheduling policy configuration
*/
setPolicy(policy: SchedulingPolicy): Promise<void>;
}
// Phase 1: TimeSafari Endorser.ch API Interfaces
export interface OffersResponse {
data: OfferSummaryRecord[];
hitLimit: boolean;
}
export interface OfferSummaryRecord {
jwtId: string;
handleId: string;
issuedAt: string;
offeredByDid: string;
recipientDid: string;
unit: string;
amount: number;
amountGiven: number;
amountGivenConfirmed: number;
objectDescription: string;
validThrough?: string;
fullClaim?: Record<string, unknown>;
}
export interface OffersToPlansResponse {
data: OfferToPlanSummaryRecord[];
hitLimit: boolean;
}
export interface OfferToPlanSummaryRecord {
jwtId: string;
planId: string;
handleId: string;
issuedAt: string;
offeredByDid: string;
unit: string;
amount: number;
amountGiven: number;
objectDescription: string;
validThrough?: string;
}
export interface PlansLastUpdatedResponse {
data: PlanSummaryWithPreviousClaim[];
hitLimit: boolean;
}
export interface PlanSummaryWithPreviousClaim {
plan: PlanSummary;
wrappedClaimBefore?: Record<string, unknown>;
}
export interface PlanSummary {
jwtId: string;
handleId: string;
name: string;
description: string;
issuerDid: string;
agentDid: string;
startTime: string;
endTime: string;
locLat?: number;
locLon?: number;
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<string, unknown>;
};
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<string, unknown>;
};
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<string, unknown>;
};
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' | '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' | 'electron';
storageType: 'plugin-managed' | 'host-managed';
jwtExpirationSeconds?: number;
apiServer?: string;
}
export interface ActiveDidChangeEvent {
activeDid: string;
timestamp: number;
source: 'host' | 'plugin';
}
// MARK: - Phase 3: TimeSafari Background Coordination Interfaces
/**
* Phase 3: Extended DailyNotificationPlugin interface with TimeSafari coordination
*/
export interface EnhancedDailyNotificationPlugin extends DailyNotificationPlugin {
// Phase 1: ActiveDid Management (already extended in parent)
// Phase 3: TimeSafari Background Coordination
coordinateBackgroundTasks(): Promise<void>;
handleAppLifecycleEvent(event: AppLifecycleEvent): Promise<void>;
getCoordinationStatus(): Promise<CoordinationStatus>;
}
/**
* Phase 3: App lifecycle events for TimeSafari coordination
*/
export type AppLifecycleEvent =
| 'app_background'
| 'app_foreground'
| 'app_resumed'
| 'app_paused'
| 'app_visibility_change'
| 'app_hidden'
| 'app_visible'
| 'app_blur'
| 'app_focus';
/**
* Phase 3: Coordination status for debugging and monitoring
*/
export interface CoordinationStatus {
platform: 'android' | 'ios' | 'electron';
coordinationActive: boolean;
coordinationPaused: boolean;
autoSync?: boolean;
appBackgrounded?: boolean;
appHidden?: boolean;
visibilityState?: DocumentVisibilityState;
focused?: boolean;
lastActiveDidChange?: number;
lastCoordinationTimestamp?: number;
lastAppBackgrounded?: number;
lastAppForegrounded?: number;
lastCoordinationSuccess?: number;
lastCoordinationFailure?: number;
coordinationErrors?: string[];
activeDidTracking?: string;
}
/**
* Phase 3: PlatformServiceMixin coordination configuration
*/
export interface PlatformServiceMixinConfig {
enableAutoCoordination?: boolean;
coordinationTimeout?: number; // Max time for coordination attempts
enableLifecycleEvents?: boolean;
enableBackgroundSync?: boolean;
enableStatePersistence?: boolean;
coordinationGracePeriod?: number; // Grace period for coordination
eventHandlers?: {
[K in AppLifecycleEvent]?: () => Promise<void>;
};
}
/**
* Phase 3: WorkManager coordination data
*/
export interface WorkManagerCoordinationData {
timesafariCoordination: boolean;
coordinationTimestamp: number;
activeDidTracking: string;
platformCoordinationVersion?: number;
coordinationTimeouts?: {
maxCoordinationAge: number;
maxExecutionTime: number;
maxRetryAge: number;
};
}
/**
* Phase 3: Background execution constraints
*/
export interface BackgroundExecutionConstraints {
devicePowerMode?: 'normal' | 'low_power' | 'critical';
appForegroundState?: 'foreground' | 'background' | 'inactive';
activeDidStability?: 'stable' | 'changing' | 'unknown';
coordinationFreshness?: 'fresh' | 'stale' | 'expired';
networkAvailability?: 'cellular' | 'wifi' | 'offline';
batteryLevel?: 'high' | 'medium' | 'low' | 'critical';
}
/**
* Phase 3: Coordination report
*/
export interface CoordinationReport {
success: boolean;
operation: string;
duration: number;
constraints: BackgroundExecutionConstraints;
errors?: string[];
timestamp: number;
activeDid?: string;
authUsed: boolean;
platformSpecific?: Record<string, unknown>;
}
/**
* Phase 3: TimeSafari state synchronization data
*/
export interface TimeSafariSyncData {
authenticationState: {
activeDid: string;
jwtExpiration?: number;
tokenRefreshNeeded: boolean;
};
notificationState: {
lastDelivery: number;
lastDeliveryId?: string;
pendingDeliveries: string[];
};
backgroundTaskState: {
lastBackgroundExecution: number;
lastCoordinationSuccess: number;
pendingCoordinationTasks: string[];
};
activeDidHistory: {
changes: Array<{
did: string;
timestamp: number;
source: string;
}>;
pendingUpdates: string[];
};
}
// MARK: - Phase 4: TimeSafari Notification Types
/**
* Phase 4: TimeSafari-specific notification interfaces
*/
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;
starredPlanIds?: string[];
lastKnownOfferId?: string;
lastKnownPlanId?: string;
fetchOffersToPerson?: boolean;
fetchOffersToProjects?: boolean;
fetchProjectUpdates?: boolean;
notificationPreferences?: {
offers: boolean;
projects: boolean;
people: boolean;
items: boolean;
};
}
// TimeSafari notification subtype types
export type TimeSafariOfferSubtype =
| 'new_to_me'
| 'changed_to_me'
| 'new_to_projects'
| 'changed_to_projects'
| 'new_to_favorites'
| 'changed_to_favorites';
export type TimeSafariProjectSubtype =
| 'local_and_new'
| 'local_and_changed'
| 'with_content_and_new'
| 'favorite_and_changed';
export type TimeSafariPersonSubtype =
| 'local_and_new'
| 'local_and_changed'
| 'with_content_and_new'
| 'favorite_and_changed';
export type TimeSafariItemSubtype =
| 'local_and_new'
| 'local_and_changed'
| 'favorite_and_changed';
// Individual notification interfaces
export interface TimeSafariOfferNotification {
type: 'offer';
subtype: TimeSafariOfferSubtype;
offer: OfferSummaryRecord; // Simplified to single type initially
relevantProjects?: PlanSummary[];
notificationPriority: 'high' | 'medium' | 'low';
timestamp: number;
}
export interface TimeSafariProjectNotification {
type: 'project';
subtype: TimeSafariProjectSubtype;
project: PlanSummary;
previousClaim?: Record<string, unknown>; // Previous claim data
notificationPriority: 'high' | 'medium' | 'low';
timestamp: number;
}
export interface TimeSafariPersonNotification {
type: 'person';
subtype: TimeSafariPersonSubtype;
person: {
did: string;
name?: string;
};
notificationPriority: 'high' | 'medium' | 'low';
timestamp: number;
personDid: string; // Add missing property
}
export interface TimeSafariItemNotification {
type: 'item';
subtype: TimeSafariItemSubtype;
item: {
id: string;
name?: string;
type?: string;
};
notificationPriority: 'high' | 'medium' | 'low';
timestamp: number;
itemId: string; // Add missing property
}
// Union type for all TimeSafari notification types
export type TimeSafariNotificationType =
| TimeSafariOfferNotification
| TimeSafariProjectNotification
| TimeSafariPersonNotification
| TimeSafariItemNotification;
// Enhanced notification interface for Phase 4
export interface EnhancedTimeSafariNotification {
type: 'offer' | 'project' | 'person' | 'item';
subtype: string;
notificationPriority: 'high' | 'medium' | 'low';
timestamp: number;
disabled: boolean;
sound: boolean;
vibration: boolean;
badge: boolean;
priority: 'low' | 'normal' | 'high';
metadata?: {
generatedAt: number;
platform: string;
userDid?: string;
preferencesVersion?: number;
fallback?: boolean;
message?: string;
};
// Type-specific properties (union approach)
offer?: OfferSummaryRecord;
project?: PlanSummary;
person?: { did: string; name?: string };
item?: { id: string; name?: string; type?: string };
relevantProjects?: PlanSummary[];
previousClaim?: Record<string, unknown>;
}