You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

746 lines
23 KiB

/**
* TimeSafari PWA - CapacitorPlatformService Clean Integration Example
*
* This example shows the ACTUAL CHANGES needed to the existing TimeSafari PWA
* CapacitorPlatformService to add DailyNotification plugin functionality.
*
* The plugin code ONLY touches Capacitor classes - no isCapacitor flags needed.
*
* @author Matthew Raymer
* @version 1.0.0
*/
// =================================================
// EXISTING TIMESAFARI PWA CODE (unchanged)
// =================================================
import { Filesystem, Directory, Encoding } from "@capacitor/filesystem";
import {
Camera,
CameraResultType,
CameraSource,
CameraDirection,
} from "@capacitor/camera";
import { Capacitor } from "@capacitor/core";
import { Share } from "@capacitor/share";
import {
SQLiteConnection,
SQLiteDBConnection,
CapacitorSQLite,
DBSQLiteValues,
} from "@capacitor-community/sqlite";
import { runMigrations } from "@/db-sql/migration";
import { QueryExecResult } from "@/interfaces/database";
import {
ImageResult,
PlatformService,
PlatformCapabilities,
} from "../PlatformService";
import { logger } from "../../utils/logger";
// =================================================
// NEW IMPORTS FOR DAILYNOTIFICATION PLUGIN
// =================================================
import { DailyNotification } from '@timesafari/daily-notification-plugin';
import { TimeSafariIntegrationService } from '@timesafari/daily-notification-plugin';
// =================================================
// EXISTING INTERFACES (unchanged)
// =================================================
interface QueuedOperation {
type: "run" | "query" | "rawQuery";
sql: string;
params: unknown[];
resolve: (value: unknown) => void;
reject: (reason: unknown) => void;
}
// =================================================
// NEW INTERFACES FOR DAILYNOTIFICATION PLUGIN
// =================================================
interface PlanSummaryAndPreviousClaim {
id: string;
title: string;
description: string;
lastUpdated: string;
previousClaim?: unknown;
}
interface StarredProjectsResponse {
data: Array<PlanSummaryAndPreviousClaim>;
hitLimit: boolean;
}
interface TimeSafariSettings {
accountDid?: string;
activeDid?: string;
apiServer?: string;
starredPlanHandleIds?: string[];
lastAckedStarredPlanChangesJwtId?: string;
[key: string]: unknown;
}
/**
* EXTENDED CapacitorPlatformService with DailyNotification Integration
*
* This shows the ACTUAL CHANGES needed to the existing TimeSafari PWA
* CapacitorPlatformService class. The plugin code ONLY touches this class.
*/
export class CapacitorPlatformService implements PlatformService {
// =================================================
// EXISTING PROPERTIES (unchanged)
// =================================================
/** Current camera direction */
private currentDirection: CameraDirection = CameraDirection.Rear;
private sqlite: SQLiteConnection;
private db: SQLiteDBConnection | null = null;
private dbName = "timesafari.sqlite";
private initialized = false;
private initializationPromise: Promise<void> | null = null;
private operationQueue: Array<QueuedOperation> = [];
private isProcessingQueue: boolean = false;
// =================================================
// NEW PROPERTIES FOR DAILYNOTIFICATION PLUGIN
// =================================================
private dailyNotificationService: DailyNotification | null = null;
private integrationService: TimeSafariIntegrationService | null = null;
private dailyNotificationInitialized = false;
// ActiveDid change tracking
private currentActiveDid: string | null = null;
// =================================================
// EXISTING CONSTRUCTOR (unchanged)
// =================================================
constructor() {
this.sqlite = new SQLiteConnection(CapacitorSQLite);
}
// =================================================
// MODIFIED METHOD: Enhanced updateActiveDid with DailyNotification Integration
// =================================================
/**
* Enhanced updateActiveDid method that handles DailyNotification plugin updates
*
* This method extends the existing updateActiveDid method to also update
* the DailyNotification plugin when the activeDid changes.
*/
async updateActiveDid(did: string): Promise<void> {
const oldDid = this.currentActiveDid;
// Update the database (existing TimeSafari pattern)
await this.dbExec(
"UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1",
[did],
);
// Update local tracking
this.currentActiveDid = did;
// Update DailyNotification plugin if initialized
if (this.dailyNotificationInitialized) {
await this.updateDailyNotificationActiveDid(did, oldDid);
}
logger.debug(
`[CapacitorPlatformService] ActiveDid updated from ${oldDid} to ${did}`
);
}
// =================================================
// NEW METHOD: Update DailyNotification Plugin ActiveDid
// =================================================
/**
* Update DailyNotification plugin when activeDid changes
*/
private async updateDailyNotificationActiveDid(newDid: string, oldDid: string | null): Promise<void> {
try {
logger.log(`[CapacitorPlatformService] Updating DailyNotification plugin activeDid from ${oldDid} to ${newDid}`);
// Get new settings for the new activeDid
const newSettings = await this.getTimeSafariSettings();
// Reconfigure DailyNotification plugin with new activeDid
await DailyNotification.configure({
timesafariConfig: {
activeDid: newDid,
endpoints: {
offersToPerson: `${newSettings.apiServer}/api/v2/offers/person`,
offersToPlans: `${newSettings.apiServer}/api/v2/offers/plans`,
projectsLastUpdated: `${newSettings.apiServer}/api/v2/report/plansLastUpdatedBetween`
},
starredProjectsConfig: {
enabled: true,
starredPlanHandleIds: newSettings.starredPlanHandleIds || [],
lastAckedJwtId: newSettings.lastAckedStarredPlanChangesJwtId || '',
fetchInterval: '0 8 * * *'
}
}
});
// Update TimeSafari Integration Service
if (this.integrationService) {
await this.integrationService.initialize({
activeDid: newDid,
storageAdapter: this.getTimeSafariStorageAdapter(),
endorserApiBaseUrl: newSettings.apiServer || 'https://endorser.ch'
});
}
logger.log(`[CapacitorPlatformService] DailyNotification plugin updated successfully for activeDid: ${newDid}`);
} catch (error) {
logger.error(`[CapacitorPlatformService] Failed to update DailyNotification plugin activeDid:`, error);
}
}
// =================================================
// NEW METHOD: Initialize DailyNotification Plugin
// =================================================
/**
* Initialize DailyNotification plugin with TimeSafari configuration
*/
async initializeDailyNotification(): Promise<void> {
if (this.dailyNotificationInitialized) {
return;
}
try {
logger.log("[CapacitorPlatformService] Initializing DailyNotification plugin...");
// Get current TimeSafari settings
const settings = await this.getTimeSafariSettings();
// Get current activeDid
const currentActiveDid = await this.getCurrentActiveDid();
// Configure DailyNotification plugin with TimeSafari data
await DailyNotification.configure({
// Basic plugin configuration
storage: 'tiered',
ttlSeconds: 1800,
enableETagSupport: true,
enableErrorHandling: true,
enablePerformanceOptimization: true,
// TimeSafari-specific configuration
timesafariConfig: {
// Use current activeDid
activeDid: currentActiveDid || '',
// Use existing TimeSafari API endpoints
endpoints: {
offersToPerson: `${settings.apiServer}/api/v2/offers/person`,
offersToPlans: `${settings.apiServer}/api/v2/offers/plans`,
projectsLastUpdated: `${settings.apiServer}/api/v2/report/plansLastUpdatedBetween`
},
// Configure starred projects fetching (matches existing TimeSafari pattern)
starredProjectsConfig: {
enabled: true,
starredPlanHandleIds: settings.starredPlanHandleIds || [],
lastAckedJwtId: settings.lastAckedStarredPlanChangesJwtId || '',
fetchInterval: '0 8 * * *', // Daily at 8 AM
maxResults: 50,
hitLimitHandling: 'warn' // Same as existing TimeSafari error handling
},
// Sync configuration (optimized for TimeSafari use case)
syncConfig: {
enableParallel: true,
maxConcurrent: 3,
batchSize: 10,
timeout: 30000,
retryAttempts: 3
},
// Error policy (matches existing TimeSafari error handling)
errorPolicy: {
maxRetries: 3,
backoffMultiplier: 2,
activeDidChangeRetries: 5, // Special retry for activeDid changes
starredProjectsRetries: 3
}
},
// Network configuration using existing TimeSafari patterns
networkConfig: {
baseURL: settings.apiServer || 'https://endorser.ch',
timeout: 30000,
retryAttempts: 3,
retryDelay: 1000,
maxConcurrent: 5,
// Headers matching TimeSafari pattern
defaultHeaders: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'User-Agent': 'TimeSafari-PWA/1.0.0'
}
},
// Content fetch configuration (replaces existing loadNewStarredProjectChanges)
contentFetch: {
enabled: true,
schedule: '0 8 * * *', // Daily at 8 AM
// Use existing TimeSafari request pattern
requestConfig: {
method: 'POST',
url: `${settings.apiServer}/api/v2/report/plansLastUpdatedBetween`,
headers: {
'Authorization': 'Bearer ${jwt}',
'X-User-DID': '${activeDid}',
'Content-Type': 'application/json'
},
body: {
planIds: '${starredPlanHandleIds}',
afterId: '${lastAckedJwtId}'
}
},
// Callbacks that match TimeSafari error handling
callbacks: {
onSuccess: this.handleStarredProjectsSuccess.bind(this),
onError: this.handleStarredProjectsError.bind(this),
onComplete: this.handleStarredProjectsComplete.bind(this)
}
}
});
// Initialize TimeSafari Integration Service
this.integrationService = TimeSafariIntegrationService.getInstance();
await this.integrationService.initialize({
activeDid: currentActiveDid || '',
storageAdapter: this.getTimeSafariStorageAdapter(),
endorserApiBaseUrl: settings.apiServer || 'https://endorser.ch',
// Use existing TimeSafari request patterns
requestConfig: {
baseURL: settings.apiServer || 'https://endorser.ch',
timeout: 30000,
retryAttempts: 3
},
// Configure starred projects fetching
starredProjectsConfig: {
enabled: true,
starredPlanHandleIds: settings.starredPlanHandleIds || [],
lastAckedJwtId: settings.lastAckedStarredPlanChangesJwtId || '',
fetchInterval: '0 8 * * *',
maxResults: 50
}
});
// Schedule daily notifications
await DailyNotification.scheduleDailyNotification({
title: 'TimeSafari Community Update',
body: 'You have new offers and project updates',
time: '09:00',
channel: 'timesafari_community_updates'
});
this.dailyNotificationInitialized = true;
this.currentActiveDid = currentActiveDid;
logger.log("[CapacitorPlatformService] DailyNotification plugin initialized successfully");
} catch (error) {
logger.error("[CapacitorPlatformService] Failed to initialize DailyNotification plugin:", error);
throw error;
}
}
// =================================================
// NEW METHOD: Get Current ActiveDid
// =================================================
/**
* Get current activeDid from the database
*/
private async getCurrentActiveDid(): Promise<string | null> {
try {
const result = await this.dbQuery(
"SELECT activeDid FROM active_identity WHERE id = 1"
);
if (result?.values?.length) {
const activeDid = result.values[0][0] as string | null;
return activeDid;
}
return null;
} catch (error) {
logger.error("[CapacitorPlatformService] Error getting current activeDid:", error);
return null;
}
}
// =================================================
// NEW METHOD: Enhanced loadNewStarredProjectChanges
// =================================================
/**
* Enhanced version of existing TimeSafari loadNewStarredProjectChanges method
*
* This method replaces the existing TimeSafari PWA method with plugin-enhanced
* functionality while maintaining the same interface and behavior.
*/
async loadNewStarredProjectChanges(): Promise<StarredProjectsResponse> {
// Ensure DailyNotification is initialized
if (!this.dailyNotificationInitialized) {
await this.initializeDailyNotification();
}
const settings = await this.getTimeSafariSettings();
const currentActiveDid = await this.getCurrentActiveDid();
if (!currentActiveDid || !settings.starredPlanHandleIds?.length) {
return { data: [], hitLimit: false };
}
try {
// Use plugin's enhanced fetching with same interface as existing TimeSafari code
const starredProjectChanges = await this.integrationService!.getStarredProjectsWithChanges(
currentActiveDid,
settings.starredPlanHandleIds,
settings.lastAckedStarredPlanChangesJwtId
);
// Enhanced logging (optional)
logger.log("[CapacitorPlatformService] Starred projects loaded successfully:", {
count: starredProjectChanges.data.length,
hitLimit: starredProjectChanges.hitLimit,
planIds: settings.starredPlanHandleIds.length,
activeDid: currentActiveDid
});
return starredProjectChanges;
} catch (error) {
// Same error handling as existing TimeSafari code
logger.warn("[CapacitorPlatformService] Failed to load starred project changes:", error);
return { data: [], hitLimit: false };
}
}
// =================================================
// NEW METHOD: Get TimeSafari Settings
// =================================================
/**
* Get TimeSafari settings using existing database patterns
*/
private async getTimeSafariSettings(): Promise<TimeSafariSettings> {
try {
// Get current activeDid
const currentActiveDid = await this.getCurrentActiveDid();
if (!currentActiveDid) {
return {};
}
// Use existing TimeSafari settings retrieval pattern
const result = await this.dbQuery(
"SELECT * FROM settings WHERE accountDid = ?",
[currentActiveDid]
);
if (!result?.values?.length) {
return {};
}
// Map database columns to values (existing TimeSafari pattern)
const settings: TimeSafariSettings = {};
result.columns.forEach((column, index) => {
if (column !== 'id') {
settings[column] = result.values[0][index];
}
});
// Set activeDid from current value
settings.activeDid = currentActiveDid;
// Handle JSON field parsing (existing TimeSafari pattern)
if (settings.starredPlanHandleIds && typeof settings.starredPlanHandleIds === 'string') {
try {
settings.starredPlanHandleIds = JSON.parse(settings.starredPlanHandleIds);
} catch {
settings.starredPlanHandleIds = [];
}
}
return settings;
} catch (error) {
logger.error("[CapacitorPlatformService] Error getting TimeSafari settings:", error);
return {};
}
}
// =================================================
// NEW METHOD: Get TimeSafari Storage Adapter
// =================================================
/**
* Get TimeSafari storage adapter using existing patterns
*/
private getTimeSafariStorageAdapter(): unknown {
// Return existing TimeSafari storage adapter
return {
// Use existing TimeSafari storage patterns
store: async (key: string, value: unknown) => {
await this.dbExec(
"INSERT OR REPLACE INTO temp (id, data) VALUES (?, ?)",
[key, JSON.stringify(value)]
);
},
retrieve: async (key: string) => {
const result = await this.dbQuery(
"SELECT data FROM temp WHERE id = ?",
[key]
);
if (result?.values?.length) {
try {
return JSON.parse(result.values[0][0] as string);
} catch {
return null;
}
}
return null;
}
};
}
// =================================================
// NEW METHODS: Callback Handlers
// =================================================
/**
* Callback handler for successful starred projects fetch
*/
private async handleStarredProjectsSuccess(data: StarredProjectsResponse): Promise<void> {
// Enhanced logging (optional)
logger.log("[CapacitorPlatformService] Starred projects success callback:", {
count: data.data.length,
hitLimit: data.hitLimit,
activeDid: this.currentActiveDid
});
// Store results in TimeSafari temp table for UI access
await this.dbExec(
"INSERT OR REPLACE INTO temp (id, data) VALUES (?, ?)",
['starred_projects_latest', JSON.stringify(data)]
);
}
/**
* Callback handler for starred projects fetch errors
*/
private async handleStarredProjectsError(error: Error): Promise<void> {
// Same error handling as existing TimeSafari code
logger.warn("[CapacitorPlatformService] Failed to load starred project changes:", error);
// Store error in TimeSafari temp table for UI access
await this.dbExec(
"INSERT OR REPLACE INTO temp (id, data) VALUES (?, ?)",
['starred_projects_error', JSON.stringify({
error: error.message,
timestamp: Date.now(),
activeDid: this.currentActiveDid
})]
);
}
/**
* Callback handler for starred projects fetch completion
*/
private async handleStarredProjectsComplete(result: unknown): Promise<void> {
// Handle completion
logger.log("[CapacitorPlatformService] Starred projects fetch completed:", {
result,
activeDid: this.currentActiveDid
});
}
// =================================================
// NEW METHOD: Get DailyNotification Status
// =================================================
/**
* Get DailyNotification plugin status for debugging
*/
async getDailyNotificationStatus(): Promise<{
initialized: boolean;
platform: string;
capabilities: PlatformCapabilities;
currentActiveDid: string | null;
}> {
return {
initialized: this.dailyNotificationInitialized,
platform: Capacitor.getPlatform(),
capabilities: this.getCapabilities(),
currentActiveDid: this.currentActiveDid
};
}
// =================================================
// MODIFIED METHOD: Enhanced initializeDatabase
// =================================================
private async initializeDatabase(): Promise<void> {
// If already initialized, return immediately
if (this.initialized) {
return;
}
// If initialization is in progress, wait for it
if (this.initializationPromise) {
return this.initializationPromise;
}
try {
// Start initialization
this.initializationPromise = this._initialize();
await this.initializationPromise;
// NEW: Initialize DailyNotification after database is ready
await this.initializeDailyNotification();
} catch (error) {
logger.error(
"[CapacitorPlatformService] Initialize database method failed:",
error,
);
this.initializationPromise = null; // Reset on failure
throw error;
}
}
// =================================================
// EXISTING METHODS (unchanged - showing key ones)
// =================================================
private async _initialize(): Promise<void> {
if (this.initialized) {
return;
}
try {
// Create/Open database
this.db = await this.sqlite.createConnection(
this.dbName,
false,
"no-encryption",
1,
false,
);
await this.db.open();
// Run migrations
await this.runCapacitorMigrations();
this.initialized = true;
logger.log(
"[CapacitorPlatformService] SQLite database initialized successfully",
);
// Start processing the queue after initialization
this.processQueue();
} catch (error) {
logger.error(
"[CapacitorPlatformService] Error initializing SQLite database:",
error,
);
throw new Error(
"[CapacitorPlatformService] Failed to initialize database",
);
}
}
// ... (all other existing methods remain unchanged)
/**
* Gets the capabilities of the Capacitor platform
* @returns Platform capabilities object
*/
getCapabilities(): PlatformCapabilities {
const platform = Capacitor.getPlatform();
return {
hasFileSystem: true,
hasCamera: true,
isMobile: true, // Capacitor is always mobile
isIOS: platform === "ios",
hasFileDownload: false, // Mobile platforms need sharing
needsFileHandlingInstructions: true, // Mobile needs instructions
isNativeApp: true,
};
}
/**
* @see PlatformService.dbQuery
*/
async dbQuery(sql: string, params?: unknown[]): Promise<QueryExecResult> {
await this.waitForInitialization();
return this.queueOperation<QueryExecResult>("query", sql, params || []);
}
/**
* @see PlatformService.dbExec
*/
async dbExec(
sql: string,
params?: unknown[],
): Promise<{ changes: number; lastId?: number }> {
await this.waitForInitialization();
return this.queueOperation<{ changes: number; lastId?: number }>(
"run",
sql,
params || [],
);
}
// ... (all other existing methods remain unchanged)
/**
* Checks if running on Capacitor platform.
* @returns true, as this is the Capacitor implementation
*/
isCapacitor(): boolean {
return true;
}
/**
* Checks if running on Electron platform.
* @returns false, as this is Capacitor, not Electron
*/
isElectron(): boolean {
return false;
}
/**
* Checks if running on web platform.
* @returns false, as this is not web
*/
isWeb(): boolean {
return false;
}
// ... (all other existing methods remain unchanged)
}