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.
 
 
 
 
 
 

16 KiB

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

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

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)

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<string, unknown>, "contacts"
  );
  await platformService.dbExec(sql, params);
}

2. Manual Contact Addition (ContactsView.vue)

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<string, unknown>, "contacts"
  );
  await platformService.dbExec(sql, params);
}

3. Contact Import (ContactImportView.vue)

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)

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

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)

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

// 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)**
// 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)
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)
// Not implemented - returns empty results
async listBackupFiles(): Promise<Array<{name: string, uri: string, size?: number, type: 'contacts' | 'seed' | 'other', path?: string}>> {
  return [];
}

4. Backup File Listing System

A. File Discovery (CapacitorPlatformService.ts)

1. Enhanced File Discovery
async listUserAccessibleFilesEnhanced(): Promise<Array<{name: string, uri: string, size?: number, path?: string}>> {
  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

async listBackupFiles(): Promise<Array<{name: string, uri: string, size?: number, type: 'contacts' | 'seed' | 'other', path?: string}>> {
  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

@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

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

{
  "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

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

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.