# TimeSafari Contact Backup System ## Overview The TimeSafari application implements a comprehensive contact backup and listing system that works across multiple platforms (Web, iOS, Android, Desktop). This document breaks down how contacts are saved, exported, and listed as backups. ## Architecture Components ### 1. Database Layer #### Contact Data Structure ```typescript interface Contact { did: string; // Decentralized Identifier (primary key) contactMethods?: ContactMethod[]; // Array of contact methods (EMAIL, SMS, etc.) name?: string; // Display name nextPubKeyHashB64?: string; // Base64 hash of next public key notes?: string; // User notes profileImageUrl?: string; // Profile image URL publicKeyBase64?: string; // Base64 encoded public key seesMe?: boolean; // Visibility setting registered?: boolean; // Registration status } interface ContactMethod { label: string; // Display label type: string; // Type (EMAIL, SMS, WHATSAPP, etc.) value: string; // Contact value } ``` #### Database Schema ```sql CREATE TABLE contacts ( id INTEGER PRIMARY KEY AUTOINCREMENT, did TEXT NOT NULL, -- Decentralized Identifier name TEXT, -- Display name contactMethods TEXT, -- JSON string of contact methods nextPubKeyHashB64 TEXT, -- Next public key hash notes TEXT, -- User notes profileImageUrl TEXT, -- Profile image URL publicKeyBase64 TEXT, -- Public key seesMe BOOLEAN, -- Visibility flag registered BOOLEAN -- Registration status ); CREATE INDEX idx_contacts_did ON contacts(did); CREATE INDEX idx_contacts_name ON contacts(name); ``` ### 2. Contact Saving Operations #### A. Adding New Contacts **1. QR Code Scanning (`ContactQRScanFullView.vue`)** ```typescript async addNewContact(contact: Contact) { // Check for existing contact const existingContacts = await platformService.dbQuery( "SELECT * FROM contacts WHERE did = ?", [contact.did] ); if (existingContact) { // Handle duplicate return; } // Convert contactMethods to JSON string for storage contact.contactMethods = JSON.stringify( parseJsonField(contact.contactMethods, []) ); // Insert into database const { sql, params } = databaseUtil.generateInsertStatement( contact as unknown as Record, "contacts" ); await platformService.dbExec(sql, params); } ``` **2. Manual Contact Addition (`ContactsView.vue`)** ```typescript private async addContact(newContact: Contact) { // Validate DID format if (!isDid(newContact.did)) { throw new Error("Invalid DID format"); } // Generate and execute INSERT statement const { sql, params } = databaseUtil.generateInsertStatement( newContact as unknown as Record, "contacts" ); await platformService.dbExec(sql, params); } ``` **3. Contact Import (`ContactImportView.vue`)** ```typescript async importContacts() { for (const contact of selectedContacts) { const contactToStore = contactToDbRecord(contact); if (existingContact) { // Update existing contact const { sql, params } = databaseUtil.generateUpdateStatement( contactToStore, "contacts", "did = ?", [contact.did] ); await platformService.dbExec(sql, params); } else { // Add new contact const { sql, params } = databaseUtil.generateInsertStatement( contactToStore, "contacts" ); await platformService.dbExec(sql, params); } } } ``` #### B. Updating Existing Contacts **Contact Editing (`ContactEditView.vue`)** ```typescript async saveEdit() { // Normalize contact methods const contactMethods = this.contactMethods.map(method => ({ ...method, type: method.type.toUpperCase() })); // Update database const contactMethodsString = JSON.stringify(contactMethods); await platformService.dbExec( "UPDATE contacts SET name = ?, notes = ?, contactMethods = ? WHERE did = ?", [this.contactName, this.contactNotes, contactMethodsString, this.contact?.did] ); } ``` ### 3. Contact Export/Backup System #### A. Export Process (`DataExportSection.vue`) #### 1. Data Retrieval ```typescript async exportDatabase() { // Query all contacts from database const result = await platformService.dbQuery("SELECT * FROM contacts"); const allContacts = databaseUtil.mapQueryResultToValues(result) as Contact[]; // Convert to export format const exportData = contactsToExportJson(allContacts); const jsonStr = JSON.stringify(exportData, null, 2); } ``` #### 2. Export Format Conversion (`libs/util.ts`) ```typescript export const contactsToExportJson = (contacts: Contact[]): DatabaseExport => { const rows = contacts.map((contact) => ({ did: contact.did, name: contact.name || null, contactMethods: contact.contactMethods ? JSON.stringify(parseJsonField(contact.contactMethods, [])) : null, nextPubKeyHashB64: contact.nextPubKeyHashB64 || null, notes: contact.notes || null, profileImageUrl: contact.profileImageUrl || null, publicKeyBase64: contact.publicKeyBase64 || null, seesMe: contact.seesMe || false, registered: contact.registered || false, })); return { data: { data: [{ tableName: "contacts", rows }] } }; }; ``` #### 3. File Generation ```typescript // Create timestamped filename const timestamp = getTimestampForFilename(); const fileName = `${AppString.APP_NAME_NO_SPACES}-backup-contacts-${timestamp}.json`; // Create blob and save const blob = new Blob([jsonStr], { type: "application/json" }); ``` #### B. Platform-Specific File Saving ##### 1. Web Platform (`WebPlatformService.ts`)** ```typescript // Uses browser download API const downloadUrl = URL.createObjectURL(blob); const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement; downloadAnchor.href = downloadUrl; downloadAnchor.download = fileName; downloadAnchor.click(); ``` ##### 2. Mobile Platforms (`CapacitorPlatformService.ts`) ```typescript async writeAndShareFile(fileName: string, content: string, options = {}) { let fileUri: string; if (options.allowLocationSelection) { // User chooses location fileUri = await this.saveWithUserChoice(fileName, content, options.mimeType); } else if (options.saveToPrivateStorage) { // Save to app-private storage const result = await Filesystem.writeFile({ path: fileName, data: content, directory: Directory.Data, encoding: Encoding.UTF8, recursive: true, }); fileUri = result.uri; } else { // Save to user-accessible location (Downloads/Documents) fileUri = await this.saveToDownloads(fileName, content); } // Share the file return await this.shareFile(fileUri, fileName); } ``` ##### 3. Desktop Platforms (`ElectronPlatformService.ts`, `PyWebViewPlatformService.ts`) ```typescript // Not implemented - returns empty results async listBackupFiles(): Promise> { return []; } ``` ### 4. Backup File Listing System #### A. File Discovery (`CapacitorPlatformService.ts`) ##### 1. Enhanced File Discovery ```typescript async listUserAccessibleFilesEnhanced(): Promise> { const allFiles: Array<{name: string, uri: string, size?: number, path?: string}> = []; if (this.getCapabilities().isIOS) { // iOS: Documents directory const result = await Filesystem.readdir({ path: ".", directory: Directory.Documents, }); const files = result.files.map((file) => ({ name: typeof file === "string" ? file : file.name, uri: `file://${file.uri || file}`, size: typeof file === "string" ? undefined : file.size, path: "Documents" })); allFiles.push(...files); } else { // Android: Multiple locations const commonPaths = ["Download", "Documents", "Backups", "TimeSafari", "Data"]; for (const path of commonPaths) { try { const result = await Filesystem.readdir({ path: path, directory: Directory.ExternalStorage, }); // Filter for TimeSafari-related files const relevantFiles = result.files .filter(file => { const fileName = typeof file === "string" ? file : file.name; const name = fileName.toLowerCase(); return name.includes('timesafari') || name.includes('backup') || name.includes('contacts') || name.endsWith('.json'); }) .map((file) => ({ name: typeof file === "string" ? file : file.name, uri: `file://${file.uri || file}`, size: typeof file === "string" ? undefined : file.size, path: path })); allFiles.push(...relevantFiles); } catch (error) { // Silently skip inaccessible directories } } } return allFiles; } ``` **2. Backup File Filtering** ```typescript async listBackupFiles(): Promise> { const allFiles = await this.listUserAccessibleFilesEnhanced(); const backupFiles = allFiles .filter(file => { const name = file.name.toLowerCase(); // Exclude directory-access notification files if (name.startsWith('timesafari-directory-access-') && name.endsWith('.txt')) { return false; } // Check backup criteria const isJson = name.endsWith('.json'); const hasTimeSafari = name.includes('timesafari'); const hasBackup = name.includes('backup'); const hasContacts = name.includes('contacts'); const hasSeed = name.includes('seed'); const hasExport = name.includes('export'); const hasData = name.includes('data'); return isJson || hasTimeSafari || hasBackup || hasContacts || hasSeed || hasExport || hasData; }) .map(file => { const name = file.name.toLowerCase(); let type: 'contacts' | 'seed' | 'other' = 'other'; // Categorize files if (name.includes('contacts') || (name.includes('timesafari') && name.includes('backup'))) { type = 'contacts'; } else if (name.includes('seed') || name.includes('mnemonic') || name.includes('private')) { type = 'seed'; } else if (name.endsWith('.json')) { type = 'other'; } return { ...file, type }; }); return backupFiles; } ``` #### B. UI Components (`BackupFilesList.vue`) **1. File Display** ```typescript @Component export default class BackupFilesList extends Vue { backupFiles: Array<{name: string, uri: string, size?: number, type: 'contacts' | 'seed' | 'other', path?: string}> = []; selectedType: 'all' | 'contacts' | 'seed' | 'other' = 'all'; isLoading = false; async refreshFiles() { this.isLoading = true; try { this.backupFiles = await this.platformService.listBackupFiles(); // Log file type distribution const typeCounts = { contacts: this.backupFiles.filter(f => f.type === 'contacts').length, seed: this.backupFiles.filter(f => f.type === 'seed').length, other: this.backupFiles.filter(f => f.type === 'other').length, total: this.backupFiles.length }; } catch (error) { // Handle error } finally { this.isLoading = false; } } } ``` **2. File Operations** ```typescript async openFile(fileUri: string, fileName: string) { const result = await this.platformService.openFile(fileUri, fileName); if (!result.success) { throw new Error(result.error || "Failed to open file"); } } async openBackupDirectory() { const result = await this.platformService.openBackupDirectory(); if (!result.success) { throw new Error(result.error || "Failed to open backup directory"); } } ``` ### 5. Platform-Specific Storage Locations #### A. iOS Platform - **Primary Location**: Documents folder (accessible via Files app) - **Persistence**: Survives app installations - **Access**: Through iOS Files app - **File Format**: JSON with timestamped filenames #### B. Android Platform - **Primary Locations**: - `Download/TimeSafari/` (external storage) - `TimeSafari/` (external storage) - User-chosen locations via file picker - **Persistence**: Survives app installations - **Access**: Through file managers - **File Format**: JSON with timestamped filenames #### C. Web Platform - **Primary Location**: Browser downloads folder - **Persistence**: Depends on browser settings - **Access**: Through browser download manager - **File Format**: JSON with timestamped filenames #### D. Desktop Platforms (Electron/PyWebView) - **Status**: Not implemented - **Fallback**: Returns empty arrays for file operations ### 6. File Naming Convention #### A. Contact Backup Files ``` TimeSafari-backup-contacts-YYYY-MM-DD-HH-MM-SS.json ``` #### B. File Content Structure ```json { "data": { "data": [ { "tableName": "contacts", "rows": [ { "did": "did:ethr:0x...", "name": "Contact Name", "contactMethods": "[{\"type\":\"EMAIL\",\"value\":\"email@example.com\"}]", "notes": "User notes", "profileImageUrl": "https://...", "publicKeyBase64": "base64...", "seesMe": true, "registered": false } ] } ] } } ``` ### 7. Error Handling and Logging #### A. Comprehensive Logging ```typescript logger.log("[CapacitorPlatformService] File write successful:", { uri: fileUri, saved, timestamp: new Date().toISOString(), }); logger.log("[BackupFilesList] Refreshed backup files:", { count: this.backupFiles.length, files: this.backupFiles.map(f => ({ name: f.name, type: f.type, path: f.path, size: f.size })), platform: this.platformCapabilities.isIOS ? "iOS" : "Android", timestamp: new Date().toISOString(), }); ``` #### B. Error Recovery ```typescript try { // File operations } catch (error) { logger.error("[CapacitorPlatformService] Failed to list backup files:", error); return []; } ``` ### 8. Security Considerations #### A. Data Privacy - Contact data is stored locally on device - No cloud synchronization of contact data - User controls visibility settings per contact - Backup files contain only user-authorized data #### B. File Access - Platform-specific permission handling - User choice for file locations - Secure storage options for sensitive data - Proper error handling for access failures ### 9. Performance Optimizations #### A. Database Operations - Indexed queries on `did` and `name` fields - Batch operations for multiple contacts - Efficient JSON serialization/deserialization - Connection pooling and reuse #### B. File Operations - Asynchronous file I/O - Efficient file discovery algorithms - Caching of file lists - Background refresh operations ## Summary The TimeSafari contact backup system provides: 1. **Robust Data Storage**: SQLite-based contact storage with proper indexing 2. **Cross-Platform Compatibility**: Works on web, iOS, Android, and desktop 3. **Flexible Export Options**: Multiple file formats and storage locations 4. **Intelligent File Discovery**: Finds backup files regardless of user-chosen locations 5. **User-Friendly Interface**: Clear categorization and easy file management 6. **Comprehensive Logging**: Detailed tracking for debugging and monitoring 7. **Security-First Design**: Privacy-preserving with user-controlled data access The system ensures that users can reliably backup and restore their contact data across different platforms while maintaining data integrity and user privacy.