/** * Platform Service Composable for TimeSafari * * Provides centralized access to platform-specific services across Vue components. * This composable encapsulates the singleton pattern and provides a clean interface * for components to access platform functionality without directly managing * the PlatformServiceFactory. * * Benefits: * - Centralized service access * - Better testability with easy mocking * - Cleaner component code * - Type safety with TypeScript * - Reactive capabilities if needed in the future * * @author Matthew Raymer * @version 1.0.0 * @since 2025-07-02 */ import { ref, readonly } from 'vue'; import { PlatformServiceFactory } from '@/services/PlatformServiceFactory'; import type { PlatformService } from '@/services/PlatformService'; import * as databaseUtil from '@/db/databaseUtil'; import { Contact } from '@/db/tables/contacts'; /** * Reactive reference to the platform service instance * This allows for potential reactive features in the future */ const platformService = ref(null); /** * Flag to track if service has been initialized */ const isInitialized = ref(false); /** * Initialize the platform service if not already done */ function initializePlatformService(): PlatformService { if (!platformService.value) { platformService.value = PlatformServiceFactory.getInstance(); isInitialized.value = true; } return platformService.value; } /** * Platform Service Composable * * Provides access to platform-specific services in a composable pattern. * This is the recommended way for Vue components to access platform functionality. * * @returns Object containing platform service and utility functions * * @example * ```typescript * // In a Vue component * import { usePlatformService } from '@/utils/usePlatformService'; * * export default { * setup() { * const { platform, dbQuery, dbExec, takePicture } = usePlatformService(); * * // Use platform methods directly * const takePhoto = async () => { * const result = await takePicture(); * console.log('Photo taken:', result); * }; * * return { takePhoto }; * } * }; * ``` */ export function usePlatformService() { // Initialize service on first use const service = initializePlatformService(); /** * Safely serialize parameters to avoid Proxy objects in native bridges * Vue's reactivity system can wrap arrays in Proxy objects which cause * "An object could not be cloned" errors in Capacitor */ const safeSerializeParams = (params?: unknown[]): unknown[] => { if (!params) return []; console.log('[usePlatformService] Original params:', params); console.log('[usePlatformService] Params toString:', params.toString()); console.log('[usePlatformService] Params constructor:', params.constructor?.name); // Use the most aggressive approach: JSON round-trip + spread operator try { // Method 1: JSON round-trip to completely strip any Proxy const jsonSerialized = JSON.parse(JSON.stringify(params)); console.log('[usePlatformService] JSON serialized:', jsonSerialized); // Method 2: Spread operator to create new array const spreadArray = [...jsonSerialized]; console.log('[usePlatformService] Spread array:', spreadArray); // Method 3: Force primitive extraction for each element const finalParams = spreadArray.map((param, index) => { if (param === null || param === undefined) { return param; } // Force convert to primitive value if (typeof param === 'object') { if (Array.isArray(param)) { return [...param]; // Spread to new array } else { return { ...param }; // Spread to new object } } return param; }); console.log('[usePlatformService] Final params:', finalParams); console.log('[usePlatformService] Final params toString:', finalParams.toString()); console.log('[usePlatformService] Final params constructor:', finalParams.constructor?.name); return finalParams; } catch (error) { console.error('[usePlatformService] Serialization error:', error); // Fallback: manual extraction const fallbackParams: unknown[] = []; for (let i = 0; i < params.length; i++) { try { // Try to access the value directly const value = params[i]; fallbackParams.push(value); } catch (accessError) { console.error('[usePlatformService] Access error for param', i, ':', accessError); fallbackParams.push(String(params[i])); } } console.log('[usePlatformService] Fallback params:', fallbackParams); return fallbackParams; } }; /** * Database query method with proper typing and safe parameter serialization */ const dbQuery = async (sql: string, params?: unknown[]) => { const safeParams = safeSerializeParams(params); return await service.dbQuery(sql, safeParams); }; /** * Database execution method with proper typing and safe parameter serialization */ const dbExec = async (sql: string, params?: unknown[]) => { const safeParams = safeSerializeParams(params); return await service.dbExec(sql, safeParams); }; /** * Get single row from database with proper typing and safe parameter serialization */ const dbGetOneRow = async (sql: string, params?: unknown[]) => { const safeParams = safeSerializeParams(params); return await service.dbGetOneRow(sql, safeParams); }; /** * Take picture with platform-specific implementation */ const takePicture = async () => { return await service.takePicture(); }; /** * Pick image from device with platform-specific implementation */ const pickImage = async () => { return await service.pickImage(); }; /** * Get platform capabilities */ const getCapabilities = () => { return service.getCapabilities(); }; /** * Platform detection methods using capabilities */ const isWeb = () => { const capabilities = service.getCapabilities(); return !capabilities.isNativeApp; }; const isCapacitor = () => { const capabilities = service.getCapabilities(); return capabilities.isNativeApp && capabilities.isMobile; }; const isElectron = () => { const capabilities = service.getCapabilities(); return capabilities.isNativeApp && !capabilities.isMobile; }; /** * File operations (where supported) */ const readFile = async (path: string) => { return await service.readFile(path); }; const writeFile = async (path: string, content: string) => { return await service.writeFile(path, content); }; const deleteFile = async (path: string) => { return await service.deleteFile(path); }; const listFiles = async (directory: string) => { return await service.listFiles(directory); }; /** * Camera operations */ const rotateCamera = async () => { return await service.rotateCamera(); }; /** * Deep link handling */ const handleDeepLink = async (url: string) => { return await service.handleDeepLink(url); }; /** * File sharing */ const writeAndShareFile = async (fileName: string, content: string) => { return await service.writeAndShareFile(fileName, content); }; // ======================================== // Higher-level database operations // ======================================== /** * Get all contacts from database with proper typing * @returns Promise Typed array of contacts */ const getContacts = async (): Promise => { const result = await dbQuery("SELECT * FROM contacts ORDER BY name"); return databaseUtil.mapQueryResultToValues(result) as Contact[]; }; /** * Get contacts with content visibility filter * @param showBlocked Whether to include blocked contacts * @returns Promise Filtered contacts */ const getContactsWithFilter = async (showBlocked = true): Promise => { const contacts = await getContacts(); return showBlocked ? contacts : contacts.filter(c => c.iViewContent !== false); }; /** * Get user settings with proper typing * @returns Promise with all user settings */ const getSettings = async () => { return await databaseUtil.retrieveSettingsForActiveAccount(); }; /** * Save default settings * @param settings Partial settings object to update */ const saveSettings = async (settings: Partial) => { return await databaseUtil.updateDefaultSettings(settings); }; /** * Save DID-specific settings * @param did DID identifier * @param settings Partial settings object to update */ const saveDidSettings = async (did: string, settings: Partial) => { return await databaseUtil.updateDidSpecificSettings(did, settings); }; /** * Get account by DID * @param did DID identifier * @returns Account data or null if not found */ const getAccount = async (did?: string) => { if (!did) return null; const result = await dbQuery("SELECT * FROM accounts WHERE did = ? LIMIT 1", [did]); const mappedResults = databaseUtil.mapQueryResultToValues(result); return mappedResults.length > 0 ? mappedResults[0] : null; }; /** * Log activity message to database * @param message Activity message to log */ const logActivity = async (message: string) => { const timestamp = new Date().toISOString(); await dbExec("INSERT INTO logs (date, message) VALUES (?, ?)", [timestamp, message]); }; return { // Direct service access (for advanced use cases) platform: readonly(platformService), isInitialized: readonly(isInitialized), // Database operations (low-level) dbQuery, dbExec, dbGetOneRow, // Database operations (high-level) getContacts, getContactsWithFilter, getSettings, saveSettings, saveDidSettings, getAccount, logActivity, // Media operations takePicture, pickImage, rotateCamera, // Platform detection isWeb, isCapacitor, isElectron, getCapabilities, // File operations readFile, writeFile, deleteFile, listFiles, writeAndShareFile, // Navigation handleDeepLink, // Raw service access for cases not covered above service }; } /** * Type helper for the composable return type */ export type PlatformServiceComposable = ReturnType;