diff --git a/.env.mobile b/.env.mobile index 33e24dc8..f3831533 100644 --- a/.env.mobile +++ b/.env.mobile @@ -1 +1,5 @@ -PLATFORM=mobile \ No newline at end of file +PLATFORM=mobile +VITE_ENDORSER_API_URL=https://test-api.endorser.ch/api/v2/claim +VITE_PARTNER_API_URL=https://test-api.partner.ch/api/v2 +VITE_IMAGE_API_URL=https://test-api.images.ch/api/v2 +VITE_PUSH_SERVER_URL=https://test-api.push.ch/api/v2 \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js index 0a908a2e..02ecf15f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -26,6 +26,10 @@ module.exports = { "no-debugger": process.env.NODE_ENV === "production" ? "error" : "warn", "@typescript-eslint/no-explicit-any": "warn", "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/no-unnecessary-type-constraint": "off" + "@typescript-eslint/no-unnecessary-type-constraint": "off", + "@typescript-eslint/no-unused-vars": ["error", { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_" + }] }, }; diff --git a/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock index 6f5da8a4..6316cbbe 100644 Binary files a/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock and b/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/android/.gradle/buildOutputCleanup/cache.properties b/android/.gradle/buildOutputCleanup/cache.properties index 1cb74a9a..e0d365f0 100644 --- a/android/.gradle/buildOutputCleanup/cache.properties +++ b/android/.gradle/buildOutputCleanup/cache.properties @@ -1,2 +1,2 @@ -#Fri Mar 21 07:27:50 UTC 2025 -gradle.version=8.2.1 +#Thu Apr 03 08:01:00 UTC 2025 +gradle.version=8.11.1 diff --git a/android/.gradle/file-system.probe b/android/.gradle/file-system.probe index d0da6143..c967c2ff 100644 Binary files a/android/.gradle/file-system.probe and b/android/.gradle/file-system.probe differ diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle index 151fee42..e3e02249 100644 --- a/android/app/capacitor.build.gradle +++ b/android/app/capacitor.build.gradle @@ -10,6 +10,8 @@ android { apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" dependencies { implementation project(':capacitor-app') + implementation project(':capacitor-filesystem') + implementation project(':capacitor-share') } diff --git a/android/app/src/main/assets/capacitor.plugins.json b/android/app/src/main/assets/capacitor.plugins.json index 21a0521e..83bccb09 100644 --- a/android/app/src/main/assets/capacitor.plugins.json +++ b/android/app/src/main/assets/capacitor.plugins.json @@ -2,5 +2,13 @@ { "pkg": "@capacitor/app", "classpath": "com.capacitorjs.plugins.app.AppPlugin" + }, + { + "pkg": "@capacitor/filesystem", + "classpath": "com.capacitorjs.plugins.filesystem.FilesystemPlugin" + }, + { + "pkg": "@capacitor/share", + "classpath": "com.capacitorjs.plugins.share.SharePlugin" } ] diff --git a/android/app/src/main/assets/public/index.html b/android/app/src/main/assets/public/index.html index 9a2af5b2..b8bc11e2 100644 --- a/android/app/src/main/assets/public/index.html +++ b/android/app/src/main/assets/public/index.html @@ -6,7 +6,7 @@ <meta name="viewport" content="width=device-width,initial-scale=1.0"> <link rel="icon" href="/favicon.ico"> <title>TimeSafari</title> - <script type="module" crossorigin src="/assets/index-CZMUlUNO.js"></script> + <script type="module" crossorigin src="/assets/index-CxCVZqQa.js"></script> </head> <body> <noscript> diff --git a/android/build.gradle b/android/build.gradle index 85a5dda2..3995fb3d 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -7,7 +7,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.2.1' + classpath 'com.android.tools.build:gradle:8.9.0' classpath 'com.google.gms:google-services:4.4.0' // NOTE: Do not place your application dependencies here; they belong diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle index 2085c863..79e75364 100644 --- a/android/capacitor.settings.gradle +++ b/android/capacitor.settings.gradle @@ -4,3 +4,9 @@ project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/ include ':capacitor-app' project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android') + +include ':capacitor-filesystem' +project(':capacitor-filesystem').projectDir = new File('../node_modules/@capacitor/filesystem/android') + +include ':capacitor-share' +project(':capacitor-share').projectDir = new File('../node_modules/@capacitor/share/android') diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index c747538f..c1d5e018 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/package-lock.json b/package-lock.json index 4f1c9854..25b24bfa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "@capacitor/android": "^6.2.0", "@capacitor/app": "^6.0.0", "@capacitor/cli": "^6.2.0", - "@capacitor/core": "^6.2.0", + "@capacitor/core": "^6.2.1", "@capacitor/filesystem": "^6.0.3", "@capacitor/ios": "^6.2.0", "@capacitor/share": "^6.0.3", @@ -13208,9 +13208,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001707", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", - "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==", + "version": "1.0.30001709", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001709.tgz", + "integrity": "sha512-NgL3vUTnDrPCZ3zTahp4fsugQ4dc7EKTSzwQDPEel6DMoMnfH2jhry9n2Zm8onbSR+f/QtKHFOA+iAQu4kbtWA==", "devOptional": true, "funding": [ { @@ -15612,9 +15612,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.129", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.129.tgz", - "integrity": "sha512-JlXUemX4s0+9f8mLqib/bHH8gOHf5elKS6KeWG3sk3xozb/JTq/RLXIv8OKUWiK4Ah00Wm88EFj5PYkFr4RUPA==", + "version": "1.5.130", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.130.tgz", + "integrity": "sha512-Ou2u7L9j2XLZbhqzyX0jWDj6gA8D3jIfVzt4rikLf3cGBa0VdReuFimBKS9tQJA4+XpeCxj1NoWlfBXzbMa9IA==", "devOptional": true, "license": "ISC" }, @@ -16037,14 +16037,14 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.5.tgz", - "integrity": "sha512-IKKP8R87pJyMl7WWamLgPkloB16dagPIdd2FjBDbyRYPKo93wS/NbCOPh6gH+ieNLC+XZrhJt/kWj0PS/DFdmg==", + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.6.tgz", + "integrity": "sha512-mUcf7QG2Tjk7H055Jk0lGBjbgDnfrvqjhXh9t2xLMSCjZVcw9Rb1V6sVNXO0th3jgeO7zllWPTNRil3JW94TnQ==", "dev": true, "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.10.2" + "synckit": "^0.11.0" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -18568,9 +18568,9 @@ } }, "node_modules/image-size": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.0.tgz", - "integrity": "sha512-4S8fwbO6w3GeCVN6OPtA9I5IGKkcDMPcKndtUlpJuCwu7JLjtj7JZpwqLuyY2nrmQT3AWsCJLSKPsc2mPBSl3w==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", + "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", "license": "MIT", "optional": true, "peer": true, @@ -23520,9 +23520,9 @@ } }, "node_modules/nostr-tools": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.11.1.tgz", - "integrity": "sha512-+Oj5t+behIkU9kh3go5wg8Aa5oR7euBU9gOItUNapJe5Gaa+KPzMuTIN+rMRK3DaZ4Zt6RM4kR/ddwstzGKf7g==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.12.0.tgz", + "integrity": "sha512-pUWEb020gTvt1XZvTa8AKNIHWFapjsv2NKyk43Ez2nnvz6WSXsrTFE0XtkNLSRBjPn6EpxumKeNiVzLz74jNSA==", "license": "Unlicense", "dependencies": { "@noble/ciphers": "^0.5.1", @@ -28778,9 +28778,9 @@ } }, "node_modules/synckit": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.10.3.tgz", - "integrity": "sha512-R1urvuyiTaWfeCggqEvpDJwAlDVdsT9NM+IP//Tk2x7qHCkSvBk/fwFgw/TLAHzZlrAnnazMcRw0ZD8HlYFTEQ==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.1.tgz", + "integrity": "sha512-fWZqNBZNNFp/7mTUy1fSsydhKsAKJ+u90Nk7kOK5Gcq9vObaqLBLjWFDBkyVU9Vvc6Y71VbOevMuGhqv02bT+Q==", "dev": true, "license": "MIT", "dependencies": { @@ -28791,7 +28791,7 @@ "node": "^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/unts" + "url": "https://opencollective.com/synckit" } }, "node_modules/synckit/node_modules/tslib": { diff --git a/package.json b/package.json index 68ad0aa1..acc99052 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "@capacitor/android": "^6.2.0", "@capacitor/app": "^6.0.0", "@capacitor/cli": "^6.2.0", - "@capacitor/core": "^6.2.0", + "@capacitor/core": "^6.2.1", "@capacitor/filesystem": "^6.0.3", "@capacitor/ios": "^6.2.0", "@capacitor/share": "^6.0.3", diff --git a/src/components/EntityIcon.vue b/src/components/EntityIcon.vue index 9c3a083a..8c3ad58a 100644 --- a/src/components/EntityIcon.vue +++ b/src/components/EntityIcon.vue @@ -7,10 +7,11 @@ import { createAvatar, StyleOptions } from "@dicebear/core"; import { avataaars } from "@dicebear/collection"; import { Vue, Component, Prop } from "vue-facing-decorator"; import { Contact } from "../db/tables/contacts"; +import { logger } from "../utils/logger"; @Component export default class EntityIcon extends Vue { - @Prop contact: Contact; + @Prop({ required: false }) contact?: Contact; @Prop entityId = ""; // overridden by contact.did or profileImageUrl @Prop iconSize = 0; @Prop profileImageUrl = ""; // overridden by contact.profileImageUrl @@ -22,7 +23,8 @@ export default class EntityIcon extends Vue { } else { const identifier = this.contact?.did || this.entityId; if (!identifier) { - return `<img src="../src/assets/blank-square.svg" class="rounded" width="${this.iconSize}" height="${this.iconSize}" />`; + const baseUrl = import.meta.env.VITE_BASE_URL || '/'; + return `<img src="${baseUrl}assets/blank-square.svg" class="rounded" width="${this.iconSize}" height="${this.iconSize}" />`; } // https://api.dicebear.com/8.x/avataaars/svg?seed= // ... does not render things with the same seed as this library. @@ -37,6 +39,12 @@ export default class EntityIcon extends Vue { return svgString; } } + + mounted() { + logger.log('EntityIcon mounted, profileImageUrl:', this.profileImageUrl); + logger.log('EntityIcon mounted, entityId:', this.entityId); + logger.log('EntityIcon mounted, iconSize:', this.iconSize); + } } </script> <style scoped></style> diff --git a/src/db/index.ts b/src/db/index.ts index 839094c3..5f9645a5 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -1,5 +1,6 @@ import BaseDexie, { Table } from "dexie"; import { encrypted, Encryption } from "@pvermeer/dexie-encrypted-addon"; +import { exportDB } from "dexie-export-import"; import * as R from "ramda"; import { Account, AccountsSchema } from "./tables/accounts"; @@ -26,19 +27,21 @@ type NonsensitiveTables = { }; // Using 'unknown' instead of 'any' for stricter typing and to avoid TypeScript warnings -export type SecretDexie<T extends unknown = SecretTable> = BaseDexie & T; -export type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T; -export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> = - BaseDexie & T; +export type SecretDexie<T extends unknown = SecretTable> = BaseDexie & T & { export: (options?: any) => Promise<Blob> }; +export type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T & { export: (options?: any) => Promise<Blob> }; +export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> = BaseDexie & T & { export: (options?: any) => Promise<Blob> }; //// Initialize the DBs, starting with the sensitive ones. // Initialize Dexie database for secret, which is then used to encrypt accountsDB export const secretDB = new BaseDexie("TimeSafariSecret") as SecretDexie; secretDB.version(1).stores(SecretSchema); +secretDB.export = (options) => exportDB(secretDB, options); // Initialize Dexie database for accounts const accountsDexie = new BaseDexie("TimeSafariAccounts") as SensitiveDexie; +accountsDexie.version(1).stores(AccountsSchema); +accountsDexie.export = (options) => exportDB(accountsDexie, options); // Instead of accountsDBPromise, use libs/util retrieveAccountMetadata or retrieveFullyDecryptedAccount // so that it's clear whether the usage needs the private key inside. @@ -54,8 +57,15 @@ export const accountsDBPromise = useSecretAndInitializeAccountsDB( //// Now initialize the other DB. -// Initialize Dexie databases for non-sensitive data +// Initialize Dexie database for non-sensitive data export const db = new BaseDexie("TimeSafari") as NonsensitiveDexie; +db.version(1).stores({ + contacts: ContactSchema.contacts, + logs: LogSchema.logs, + settings: SettingsSchema.settings, + temp: TempSchema.temp, +}); +db.export = (options) => exportDB(db, options); // Only the tables with index modifications need listing. https://dexie.org/docs/Tutorial/Design#database-versioning diff --git a/src/interfaces/identifier.ts b/src/interfaces/identifier.ts index 969a7928..7c373227 100644 --- a/src/interfaces/identifier.ts +++ b/src/interfaces/identifier.ts @@ -15,8 +15,20 @@ export interface IKey { meta?: KeyMeta; } +export interface IService { + id: string; + type: string; + serviceEndpoint: string; + description?: string; + metadata?: { + version?: string; + capabilities?: string[]; + config?: Record<string, unknown>; + }; +} + export interface IIdentifier { did: string; keys: IKey[]; - services: any[]; + services: IService[]; } diff --git a/src/interfaces/service.ts b/src/interfaces/service.ts new file mode 100644 index 00000000..2059a415 --- /dev/null +++ b/src/interfaces/service.ts @@ -0,0 +1,288 @@ +/** + * @file service.ts + * @description Service interfaces for Decentralized Identifiers (DIDs) + * + * This module defines the service interfaces used in the TimeSafari application. + * Services are associated with DIDs to provide additional functionality and endpoints. + * + * Architecture: + * 1. Base IService interface defines common service properties + * 2. Specialized interfaces extend IService for specific service types + * 3. Services are stored in IIdentifier.services array + * 4. Services are loaded and managed by PlatformServiceFactory + * + * Service Types: + * - EndorserService: Handles claims and endorsements + * - PushNotificationService: Manages web push notifications + * - ProfileService: Handles user profiles and settings + * - BackupService: Manages data backup and restore + * + * @see IIdentifier + * @see PlatformServiceFactory + * @see DatabaseBackupService + */ + +/** + * Base interface for all DID services + * + * This interface defines the core properties that all services must implement. + * It follows the W3C DID specification for service endpoints. + * + * @example + * const service: IService = { + * id: 'endorser-service', + * type: 'EndorserService', + * serviceEndpoint: 'https://api.endorser.ch', + * description: 'Endorser service for claims and endorsements', + * metadata: { + * version: '1.0.0', + * capabilities: ['claims', 'endorsements'], + * config: { apiServer: 'https://api.endorser.ch' } + * } + * }; + */ +export interface IService { + /** + * Unique identifier for the service + * @example 'endorser-service' + * @example 'push-notification-service' + */ + id: string; + + /** + * Type of service + * @example 'EndorserService' + * @example 'PushNotificationService' + */ + type: string; + + /** + * Endpoint URL for the service + * @example 'https://api.endorser.ch' + * @example 'https://push.timesafari.app' + */ + serviceEndpoint: string; + + /** + * Optional human-readable description of the service + * @example 'Service for handling claims and endorsements' + */ + description?: string; + + /** + * Optional metadata for service configuration + */ + metadata?: { + /** + * Service version in semantic versioning format + * @example '1.0.0' + */ + version?: string; + + /** + * Array of service capabilities + * @example ['claims', 'endorsements'] + * @example ['notifications', 'alerts'] + */ + capabilities?: string[]; + + /** + * Service-specific configuration + * @example { apiServer: 'https://api.endorser.ch' } + */ + config?: Record<string, unknown>; + }; +} + +/** + * Service for handling claims and endorsements + * + * This service provides endpoints for: + * - Submitting claims + * - Managing endorsements + * - Checking rate limits + * + * @example + * const endorserService: IEndorserService = { + * id: 'endorser-service', + * type: 'EndorserService', + * serviceEndpoint: 'https://api.endorser.ch', + * metadata: { + * version: '1.0.0', + * capabilities: ['claims', 'endorsements'], + * config: { + * apiServer: 'https://api.endorser.ch', + * rateLimits: { + * claimsPerDay: 100, + * endorsementsPerDay: 1000 + * } + * } + * } + * }; + */ +export interface IEndorserService extends IService { + /** @override */ + type: "EndorserService"; + + /** @override */ + metadata: { + version: string; + capabilities: ["claims", "endorsements"]; + config: { + /** + * API server URL + * @example 'https://api.endorser.ch' + */ + apiServer: string; + + /** + * Optional rate limits + */ + rateLimits?: { + /** + * Maximum claims per day + * @default 100 + */ + claimsPerDay: number; + + /** + * Maximum endorsements per day + * @default 1000 + */ + endorsementsPerDay: number; + }; + }; + }; +} + +/** + * Service for managing web push notifications + * + * This service provides endpoints for: + * - Registering push subscriptions + * - Sending push notifications + * - Managing notification preferences + * + * @example + * const pushService: IPushNotificationService = { + * id: 'push-service', + * type: 'PushNotificationService', + * serviceEndpoint: 'https://push.timesafari.app', + * metadata: { + * version: '1.0.0', + * capabilities: ['notifications'], + * config: { + * pushServer: 'https://push.timesafari.app', + * vapidPublicKey: '...' + * } + * } + * }; + */ +export interface IPushNotificationService extends IService { + /** @override */ + type: "PushNotificationService"; + + /** @override */ + metadata: { + version: string; + capabilities: ["notifications"]; + config: { + /** + * Push server URL + * @example 'https://push.timesafari.app' + */ + pushServer: string; + + /** + * Optional VAPID public key for push notifications + */ + vapidPublicKey?: string; + }; + }; +} + +/** + * Service for managing user profiles and settings + * + * This service provides endpoints for: + * - Managing user profiles + * - Updating user settings + * - Retrieving user preferences + * + * @example + * const profileService: IProfileService = { + * id: 'profile-service', + * type: 'ProfileService', + * serviceEndpoint: 'https://partner-api.endorser.ch', + * metadata: { + * version: '1.0.0', + * capabilities: ['profile', 'settings'], + * config: { + * partnerApiServer: 'https://partner-api.endorser.ch' + * } + * } + * }; + */ +export interface IProfileService extends IService { + /** @override */ + type: "ProfileService"; + + /** @override */ + metadata: { + version: string; + capabilities: ["profile", "settings"]; + config: { + /** + * Partner API server URL + * @example 'https://partner-api.endorser.ch' + */ + partnerApiServer: string; + }; + }; +} + +/** + * Service for managing data backup and restore operations + * + * This service provides endpoints for: + * - Creating backups + * - Restoring from backups + * - Managing backup storage + * + * @example + * const backupService: IBackupService = { + * id: 'backup-service', + * type: 'BackupService', + * serviceEndpoint: 'https://backup.timesafari.app', + * metadata: { + * version: '1.0.0', + * capabilities: ['backup', 'restore'], + * config: { + * storageType: 'cloud', + * encryptionKey: '...' + * } + * } + * }; + */ +export interface IBackupService extends IService { + /** @override */ + type: "BackupService"; + + /** @override */ + metadata: { + version: string; + capabilities: ["backup", "restore"]; + config: { + /** + * Storage type for backups + * @default 'local' + */ + storageType: "local" | "cloud"; + + /** + * Optional encryption key for backups + */ + encryptionKey?: string; + }; + }; +} diff --git a/src/platforms/capacitor/DatabaseBackupService.ts b/src/platforms/capacitor/DatabaseBackupService.ts new file mode 100644 index 00000000..09326a75 --- /dev/null +++ b/src/platforms/capacitor/DatabaseBackupService.ts @@ -0,0 +1,69 @@ +/** + * @file DatabaseBackupService.ts + * @description Capacitor-specific implementation of DatabaseBackupService + * + * This implementation handles database backup operations specifically for Capacitor + * platforms (Android/iOS). It uses the Filesystem and Share plugins to save and + * share the backup file. + */ + +import { DatabaseBackupService as BaseDatabaseBackupService } from "../../services/DatabaseBackupService"; +import { Filesystem, Directory } from "@capacitor/filesystem"; +import { Share } from "@capacitor/share"; +import { log, error } from "../../utils/logger"; + +export class DatabaseBackupService extends BaseDatabaseBackupService { + /** + * Handles the backup process for Capacitor platforms + * + * @param base64Data - Backup data in base64 format + * @param arrayBuffer - Backup data as ArrayBuffer + * @param blob - Backup data as Blob + */ + protected async handleBackup( + base64Data: string, + arrayBuffer: ArrayBuffer, + blob: Blob + ): Promise<void> { + try { + log("Starting Capacitor backup process"); + + // Create a temporary file + const fileName = `timesafari-backup-${new Date().toISOString()}.json`; + const filePath = `backups/${fileName}`; + + log("Writing backup file"); + const result = await Filesystem.writeFile({ + path: filePath, + data: base64Data, + directory: Directory.Cache, + recursive: true + }); + + log("Getting file path"); + const fileInfo = await Filesystem.stat({ + path: filePath, + directory: Directory.Cache + }); + + log("Sharing backup file"); + await Share.share({ + title: "TimeSafari Backup", + text: "Your TimeSafari backup file", + url: fileInfo.uri, + dialogTitle: "Share TimeSafari Backup" + }); + + log("Backup shared successfully"); + + // Clean up the temporary file + await Filesystem.deleteFile({ + path: filePath, + directory: Directory.Cache + }); + } catch (err) { + error("Error during Capacitor backup:", err); + throw err; + } + } +} \ No newline at end of file diff --git a/src/services/DatabaseBackupService.ts b/src/services/DatabaseBackupService.ts index 9f92a8b7..e0867d89 100644 --- a/src/services/DatabaseBackupService.ts +++ b/src/services/DatabaseBackupService.ts @@ -1,26 +1,95 @@ /** * @file DatabaseBackupService.ts * @description Base service class for handling database backup operations - * @author Matthew Raymer - * @version 1.0.0 + * + * This service implements the Template Method pattern to provide a common interface + * for database backup operations across different platforms. It defines the structure + * of backup operations while delegating platform-specific implementations to subclasses. + * + * Build Process Integration: + * 1. Platform-Specific Implementation: + * - Each platform (web, electron, capacitor) has its own implementation + * - Implementations are loaded dynamically via PlatformServiceFactory + * - Located in ./platforms/{platform}/DatabaseBackupService.ts + * + * 2. Build Configuration: + * - Vite config files (vite.config.*.mts) set VITE_PLATFORM + * - PlatformServiceFactory uses this to load correct implementation + * - Build process creates separate chunks for each platform + * + * 3. Data Handling: + * - Supports multiple data formats (base64, ArrayBuffer, Blob) + * - Platform implementations handle format conversion + * - Ensures consistent backup format across platforms + * + * Usage: + * - Create backup: DatabaseBackupService.createAndShareBackup(data) + * - Platform-specific: new WebDatabaseBackupService().handleBackup() + * + * @see PlatformServiceFactory.ts + * @see vite.config.web.mts + * @see vite.config.electron.mts + * @see vite.config.capacitor.mts */ import { PlatformServiceFactory } from "./PlatformServiceFactory"; +import { log, error } from "../utils/logger"; export class DatabaseBackupService { - protected async handleBackup(): Promise<void> { + /** + * Template method that must be implemented by platform-specific services + * @param base64Data - Backup data in base64 format + * @param arrayBuffer - Backup data as ArrayBuffer + * @param blob - Backup data as Blob + * @throws Error if not implemented by subclass + */ + protected async handleBackup( + _base64Data: string, + _arrayBuffer: ArrayBuffer, + _blob: Blob, + ): Promise<void> { throw new Error( "handleBackup must be implemented by platform-specific service", ); } + /** + * Factory method to create and share a backup + * Uses PlatformServiceFactory to get platform-specific implementation + * + * @param base64Data - Backup data in base64 format + * @param arrayBuffer - Backup data as ArrayBuffer + * @param blob - Backup data as Blob + * @returns Promise that resolves when backup is complete + */ public static async createAndShareBackup( base64Data: string, arrayBuffer: ArrayBuffer, - blob: Blob, + blob: Blob ): Promise<void> { + try { + log('Creating platform-specific backup service'); + const backupService = await this.getPlatformSpecificBackupService(); + log('Backup service created successfully'); + + log('Executing platform-specific backup'); + await backupService.handleBackup(base64Data, arrayBuffer, blob); + log('Backup completed successfully'); + } catch (err) { + error('Error during backup creation:', err); + if (err instanceof Error) { + error('Error details:', { + name: err.name, + message: err.message, + stack: err.stack + }); + } + throw err; + } + } + + private static async getPlatformSpecificBackupService(): Promise<DatabaseBackupService> { const factory = PlatformServiceFactory.getInstance(); - const service = await factory.createDatabaseBackupService(); - await service.handleBackup(base64Data, arrayBuffer, blob); + return await factory.createDatabaseBackupService(); } } diff --git a/src/services/PlatformServiceFactory.ts b/src/services/PlatformServiceFactory.ts index 9c0c1646..e537e1fd 100644 --- a/src/services/PlatformServiceFactory.ts +++ b/src/services/PlatformServiceFactory.ts @@ -1,20 +1,102 @@ /** * @file PlatformServiceFactory.ts * @description Factory for creating platform-specific service implementations - * @author Matthew Raymer - * @version 1.0.0 + * + * This factory implements the Abstract Factory pattern to create platform-specific + * implementations of services. It uses Vite's dynamic import feature to load the + * appropriate implementation based on the current platform (web, electron, etc.). + * + * Architecture: + * 1. Singleton Pattern: + * - Ensures only one factory instance exists + * - Manages platform-specific service instances + * - Maintains consistent state across the application + * + * 2. Dynamic Loading: + * - Uses Vite's dynamic import for platform-specific code + * - Loads services on-demand based on platform + * - Handles platform detection and service instantiation + * + * 3. Platform Detection: + * - Uses VITE_PLATFORM environment variable + * - Supports web, electron, and capacitor platforms + * - Falls back to 'web' if platform is not specified + * + * Usage: + * ```typescript + * // Get factory instance + * const factory = PlatformServiceFactory.getInstance(); + * + * // Create platform-specific service + * const backupService = await factory.createDatabaseBackupService(); + * ``` + * + * @see vite.config.web.mts + * @see vite.config.electron.mts + * @see vite.config.capacitor.mts + * @see DatabaseBackupService */ import { DatabaseBackupService } from "./DatabaseBackupService"; +import { logger } from "../utils/logger"; +/** + * Factory class for creating platform-specific service implementations + * + * This class manages the creation and instantiation of platform-specific + * service implementations. It uses the Abstract Factory pattern to provide + * a consistent interface for creating services across different platforms. + * + * @example + * ```typescript + * // Get factory instance + * const factory = PlatformServiceFactory.getInstance(); + * + * // Create platform-specific service + * const backupService = await factory.createDatabaseBackupService(); + * + * // Use the service + * await backupService.handleBackup(data); + * ``` + */ export class PlatformServiceFactory { + /** + * Singleton instance of the factory + * @private + */ private static instance: PlatformServiceFactory; + + /** + * Current platform identifier + * @private + */ private platform: string; + /** + * Private constructor to enforce singleton pattern + * + * Initializes the factory with the current platform from environment variables. + * Falls back to 'web' if no platform is specified. + * + * @private + */ private constructor() { this.platform = import.meta.env.VITE_PLATFORM || "web"; } + /** + * Gets the singleton instance of the factory + * + * Creates a new instance if one doesn't exist, otherwise returns + * the existing instance. + * + * @returns {PlatformServiceFactory} The singleton factory instance + * + * @example + * ```typescript + * const factory = PlatformServiceFactory.getInstance(); + * ``` + */ public static getInstance(): PlatformServiceFactory { if (!PlatformServiceFactory.instance) { PlatformServiceFactory.instance = new PlatformServiceFactory(); @@ -22,19 +104,75 @@ export class PlatformServiceFactory { return PlatformServiceFactory.instance; } + /** + * Creates a platform-specific database backup service + * + * Dynamically loads and instantiates the appropriate implementation + * based on the current platform. The implementation is loaded from + * the platforms/{platform}/DatabaseBackupService.js file. + * + * @returns {Promise<DatabaseBackupService>} A promise that resolves to a platform-specific backup service + * @throws {Error} If the service fails to load or instantiate + * + * @example + * ```typescript + * const factory = PlatformServiceFactory.getInstance(); + * try { + * const backupService = await factory.createDatabaseBackupService(); + * await backupService.handleBackup(data); + * } catch (error) { + * logger.error('Failed to create backup service:', error); + * } + * ``` + */ public async createDatabaseBackupService(): Promise<DatabaseBackupService> { try { - // Use Vite's dynamic import for platform-specific implementation - const { default: PlatformService } = await import( - `./platforms/${this.platform}/DatabaseBackupService` + logger.log(`Loading platform-specific service for ${this.platform}`); + // Update the import path to point to the correct location + const { DatabaseBackupService: PlatformService } = await import( + `../platforms/${this.platform}/DatabaseBackupService` ); + logger.log('Platform service loaded successfully'); return new PlatformService(); } catch (error) { - console.error( + logger.error( `Failed to load platform-specific service for ${this.platform}:`, error, ); throw error; } } + + /** + * Gets the current platform identifier + * + * @returns {string} The current platform identifier + * + * @example + * ```typescript + * const factory = PlatformServiceFactory.getInstance(); + * logger.log(factory.getPlatform()); // 'web', 'electron', or 'capacitor' + * ``` + */ + public getPlatform(): string { + return this.platform; + } + + /** + * Sets the current platform identifier + * + * This method is primarily used for testing purposes to override + * the platform detection. Use with caution in production code. + * + * @param {string} platform - The platform identifier to set + * + * @example + * ```typescript + * const factory = PlatformServiceFactory.getInstance(); + * factory.setPlatform('electron'); // For testing purposes only + * ``` + */ + public setPlatform(platform: string): void { + this.platform = platform; + } } diff --git a/src/services/RateLimitsService.ts b/src/services/RateLimitsService.ts index 1ac5f1b7..6a95c156 100644 --- a/src/services/RateLimitsService.ts +++ b/src/services/RateLimitsService.ts @@ -8,32 +8,40 @@ import { logger } from "../utils/logger"; import { getHeaders } from "../libs/endorserServer"; import type { EndorserRateLimits, ImageRateLimits } from "../interfaces/limits"; +import axios from "axios"; export class RateLimitsService { /** * Fetches rate limits for a given DID * @param apiServer - The API server URL - * @param activeDid - The user's active DID + * @param did - The user's DID * @returns Promise<EndorserRateLimits> */ - static async fetchRateLimits( - apiServer: string, - activeDid: string, - ): Promise<EndorserRateLimits> { + static async fetchRateLimits(apiServer: string, did: string): Promise<EndorserRateLimits> { + logger.log('Fetching rate limits for DID:', did); + logger.log('Using API server:', apiServer); + try { - const headers = await getHeaders(activeDid); - const response = await fetch( - `${apiServer}/api/endorser/rateLimits/${activeDid}`, - { headers }, - ); - - if (!response.ok) { - throw new Error(`Failed to fetch rate limits: ${response.statusText}`); - } - - return await response.json(); + const headers = await getHeaders(did); + const response = await axios.get(`${apiServer}/api/v2/rate-limits/${did}`, { headers }); + logger.log('Rate limits response:', response.data); + return response.data; } catch (error) { - logger.error("Error fetching rate limits:", error); + if (axios.isAxiosError(error) && (error.response?.status === 400 || error.response?.status === 404)) { + const errorData = error.response.data as { error?: { message?: string, code?: string } }; + if (errorData.error?.code === 'UNREGISTERED_USER' || error.response?.status === 404) { + logger.log('User is not registered, returning default limits'); + return { + doneClaimsThisWeek: "0", + maxClaimsPerWeek: "0", + nextWeekBeginDateTime: new Date().toISOString(), + doneRegistrationsThisMonth: "0", + maxRegistrationsPerMonth: "0", + nextMonthBeginDateTime: new Date().toISOString() + }; + } + } + logger.error('Error fetching rate limits:', error); throw error; } } diff --git a/src/services/platforms/mobile/DatabaseBackupService.ts b/src/services/platforms/mobile/DatabaseBackupService.ts index 6f962ee0..b18d136c 100644 --- a/src/services/platforms/mobile/DatabaseBackupService.ts +++ b/src/services/platforms/mobile/DatabaseBackupService.ts @@ -10,7 +10,11 @@ import { Filesystem } from "@capacitor/filesystem"; import { Share } from "@capacitor/share"; export default class MobileDatabaseBackupService extends DatabaseBackupService { - protected async handleBackup(base64Data: string): Promise<void> { + protected async handleBackup( + base64Data: string, + _arrayBuffer: ArrayBuffer, + _blob: Blob, + ): Promise<void> { // Mobile platform handling const fileName = `database-backup-${new Date().toISOString()}.json`; const path = `backups/${fileName}`; diff --git a/src/services/platforms/web/DatabaseBackupService.ts b/src/services/platforms/web/DatabaseBackupService.ts index f0f41942..4da346dd 100644 --- a/src/services/platforms/web/DatabaseBackupService.ts +++ b/src/services/platforms/web/DatabaseBackupService.ts @@ -6,17 +6,28 @@ */ import { DatabaseBackupService } from "../../DatabaseBackupService"; +import { log, error } from "../../../utils/logger"; export default class WebDatabaseBackupService extends DatabaseBackupService { - protected async handleBackup(blob: Blob): Promise<void> { - // Web platform handling - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = `database-backup-${new Date().toISOString()}.json`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); + protected async handleBackup( + _base64Data: string, + _arrayBuffer: ArrayBuffer, + blob: Blob + ): Promise<void> { + try { + log('Starting web platform backup'); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `database-backup-${new Date().toISOString()}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + log('Web platform backup completed'); + } catch (err) { + error('Error during web platform backup:', err); + throw err; + } } } diff --git a/src/types/interfaces.ts b/src/types/interfaces.ts index c751e7d8..950d65d8 100644 --- a/src/types/interfaces.ts +++ b/src/types/interfaces.ts @@ -1,28 +1,286 @@ /** - * Type declarations for custom interfaces used in the application. - * @author Matthew Raymer + * @file interfaces.ts + * @description Core type declarations for the TimeSafari application + * + * This module defines the core interfaces and types used throughout the application. + * It serves as the central location for type definitions that are shared across + * multiple components and services. + * + * Architecture: + * 1. DID (Decentralized Identifier) Types: + * - IIdentifier: Core DID structure + * - IKey: Cryptographic key information + * - IService: Service endpoints and capabilities + * + * 2. Verifiable Credential Types: + * - GenericCredWrapper: Base wrapper for all credentials + * - GiveVerifiableCredential: Gift-related credentials + * - OfferVerifiableCredential: Offer-related credentials + * - RegisterVerifiableCredential: Registration credentials + * + * 3. Service Types: + * - EndorserService: Claims and endorsements + * - PushNotificationService: Web push notifications + * - ProfileService: User profiles + * - BackupService: Data backup + * + * @see src/interfaces/identifier.ts + * @see src/interfaces/claims.ts + * @see src/interfaces/limits.ts */ import { GiveVerifiableCredential } from "../interfaces"; +/** + * Interface for a Decentralized Identifier (DID) + * + * This interface defines the structure of a DID, which is a unique identifier + * that can be used to look up a DID document containing information associated + * with the DID, such as public keys and service endpoints. + * + * @example + * ```typescript + * const identifier: IIdentifier = { + * did: 'did:ethr:0x123...', + * provider: 'ethr', + * keys: [{ + * kid: 'keys-1', + * kms: 'local', + * type: 'Secp256k1', + * publicKeyHex: '0x...', + * meta: { derivationPath: "m/44'/60'/0'/0/0" } + * }], + * services: [{ + * id: 'endorser-service', + * type: 'EndorserService', + * serviceEndpoint: 'https://api.endorser.ch' + * }] + * }; + * ``` + */ export interface IIdentifier { + /** + * The DID string in the format 'did:method:identifier' + * @example 'did:ethr:0x123...' + */ did: string; + + /** + * The DID method provider + * @example 'ethr' + */ provider: string; + + /** + * Array of cryptographic keys associated with the DID + */ keys: Array<{ + /** + * Key identifier + * @example 'keys-1' + */ kid: string; + + /** + * Key management system + * @example 'local' + */ kms: string; + + /** + * Key type + * @example 'Secp256k1' + */ type: string; + + /** + * Public key in hexadecimal format + * @example '0x...' + */ publicKeyHex: string; + + /** + * Optional metadata about the key + */ meta?: any; }>; + + /** + * Array of service endpoints associated with the DID + */ services: Array<{ + /** + * Service identifier + * @example 'endorser-service' + */ id: string; + + /** + * Service type + * @example 'EndorserService' + */ type: string; + + /** + * Service endpoint URL + * @example 'https://api.endorser.ch' + */ serviceEndpoint: string; + + /** + * Optional service description + */ description?: string; }>; } +/** + * Interface for a cryptographic key + * + * This interface defines the structure of a cryptographic key used in the + * DID system. It includes both public and private key information, along + * with metadata about the key's purpose and derivation. + * + * @example + * ```typescript + * const key: IKey = { + * id: 'did:ethr:0x123...#keys-1', + * type: 'Secp256k1VerificationKey2018', + * controller: 'did:ethr:0x123...', + * ethereumAddress: '0x123...', + * publicKeyHex: '0x...', + * privateKeyHex: '0x...', + * meta: { + * derivationPath: "m/44'/60'/0'/0/0" + * } + * }; + * ``` + */ +export interface IKey { + /** + * Unique identifier for the key + * @example 'did:ethr:0x123...#keys-1' + */ + id: string; + + /** + * Key type specification + * @example 'Secp256k1VerificationKey2018' + */ + type: string; + + /** + * DID that controls this key + * @example 'did:ethr:0x123...' + */ + controller: string; + + /** + * Associated Ethereum address + * @example '0x123...' + */ + ethereumAddress: string; + + /** + * Public key in hexadecimal format + * @example '0x...' + */ + publicKeyHex: string; + + /** + * Private key in hexadecimal format + * @example '0x...' + */ + privateKeyHex: string; + + /** + * Optional metadata about the key + */ + meta?: { + /** + * HD wallet derivation path + * @example "m/44'/60'/0'/0/0" + */ + derivationPath?: string; + + /** + * Additional key metadata + */ + [key: string]: unknown; + }; +} + +/** + * Interface for a service endpoint + * + * This interface defines the structure of a service endpoint that can be + * associated with a DID. Services provide additional functionality and + * endpoints for DID operations. + * + * @example + * ```typescript + * const service: IService = { + * id: 'endorser-service', + * type: 'EndorserService', + * serviceEndpoint: 'https://api.endorser.ch', + * description: 'Service for handling claims and endorsements', + * metadata: { + * version: '1.0.0', + * capabilities: ['claims', 'endorsements'], + * config: { + * apiServer: 'https://api.endorser.ch' + * } + * } + * }; + * ``` + */ +export interface IService { + /** + * Unique identifier for the service + * @example 'endorser-service' + */ + id: string; + + /** + * Type of service + * @example 'EndorserService' + */ + type: string; + + /** + * Service endpoint URL + * @example 'https://api.endorser.ch' + */ + serviceEndpoint: string; + + /** + * Optional human-readable description + */ + description?: string; + + /** + * Optional service metadata + */ + metadata?: { + /** + * Service version + * @example '1.0.0' + */ + version?: string; + + /** + * Array of service capabilities + * @example ['claims', 'endorsements'] + */ + capabilities?: string[]; + + /** + * Service-specific configuration + */ + config?: Record<string, unknown>; + }; +} + export interface ExportProgress { status: "preparing" | "exporting" | "complete" | "error"; message?: string; diff --git a/src/utils/logger.ts b/src/utils/logger.ts index fcf0847a..f081b055 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -19,28 +19,55 @@ function safeStringify(obj: unknown) { }); } +function formatMessage(message: string, ...args: unknown[]): string { + const prefix = '[TimeSafari]'; + const argsString = args.length > 0 ? " - " + safeStringify(args) : ""; + return `${prefix} ${message}${argsString}`; +} + export const logger = { log: (message: string, ...args: unknown[]) => { if (process.env.NODE_ENV !== "production") { - // eslint-disable-next-line no-console - console.log(message, ...args); - const argsString = args.length > 0 ? " - " + safeStringify(args) : ""; - logToDb(message + argsString); + const formattedMessage = formatMessage(message, ...args); + console.log(formattedMessage); + logToDb(message + (args.length > 0 ? " - " + safeStringify(args) : "")); } }, warn: (message: string, ...args: unknown[]) => { if (process.env.NODE_ENV !== "production") { - // eslint-disable-next-line no-console - console.warn(message, ...args); - const argsString = args.length > 0 ? " - " + safeStringify(args) : ""; - logToDb(message + argsString); + const formattedMessage = formatMessage(message, ...args); + console.warn(formattedMessage); + logToDb(message + (args.length > 0 ? " - " + safeStringify(args) : "")); } }, error: (message: string, ...args: unknown[]) => { - // Errors will always be logged - // eslint-disable-next-line no-console - console.error(message, ...args); - const argsString = args.length > 0 ? " - " + safeStringify(args) : ""; - logToDb(message + argsString); + const formattedMessage = formatMessage(message, ...args); + console.error(formattedMessage); + logToDb(message + (args.length > 0 ? " - " + safeStringify(args) : "")); }, }; + +export function log(...args: any[]) { + const message = formatMessage(args[0], ...args.slice(1)); + console.log(message); +} + +export function error(...args: any[]) { + const message = formatMessage(args[0], ...args.slice(1)); + console.error(message); +} + +export function warn(...args: any[]) { + const message = formatMessage(args[0], ...args.slice(1)); + console.warn(message); +} + +export function info(...args: any[]) { + const message = formatMessage(args[0], ...args.slice(1)); + console.info(message); +} + +export function debug(...args: any[]) { + const message = formatMessage(args[0], ...args.slice(1)); + console.debug(message); +} diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index 072cf6ce..2b21010d 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -836,7 +836,7 @@ import "leaflet/dist/leaflet.css"; import { AxiosError } from "axios"; import { Buffer } from "buffer/"; import Dexie from "dexie"; -import "dexie-export-import"; +import { exportDB } from "dexie-export-import"; import * as R from "ramda"; import type { IIdentifier, UserProfile } from "@/types/interfaces"; import { ref } from "vue"; @@ -889,6 +889,7 @@ import { DatabaseBackupService } from "../services/DatabaseBackupService"; import { ProfileService } from "../services/ProfileService"; import { RateLimitsService } from "../services/RateLimitsService"; import ProfileSection from "../components/ProfileSection.vue"; +import { log, error } from '../utils/logger'; const inputImportFileNameRef = ref<Blob>(); @@ -943,6 +944,8 @@ export default class AccountViewView extends Vue { DEFAULT_IMAGE_API_SERVER = DEFAULT_IMAGE_API_SERVER; DEFAULT_PARTNER_API_SERVER = DEFAULT_PARTNER_API_SERVER; + private db = db; + activeDid = ""; apiServer = ""; apiServerInput = ""; @@ -1400,42 +1403,52 @@ export default class AccountViewView extends Vue { */ async exportDatabase() { try { - // Generate database blob - const blob = await (Dexie as any).export(db, { - prettyJson: true, + logger.log("Starting database export process"); + + const db = await this.getDatabase(); + logger.log("Database instance:", { + name: db.name, + version: db.verno, + tables: db.tables.map((t: { name: string }) => t.name) }); - // Convert blob to base64 for mobile platforms - const arrayBuffer = await blob.arrayBuffer(); - const base64Data = Buffer.from(arrayBuffer).toString("base64"); + if (!db.export) { + logger.error("Database export method not available"); + return; + } - await DatabaseBackupService.createAndShareBackup( - base64Data, - arrayBuffer, - blob, - ); + logger.log("Creating export blob"); + const blob = await db.export(); + logger.log("Blob created:", { + type: blob.type, + size: blob.size + }); - this.$notify( - { - group: "alert", - type: "success", - title: "Export Complete", - text: "Your database has been exported successfully.", - }, - 5000, - ); - } catch (error: unknown) { - if (error instanceof Error) { - const errorMessage = error.message; - this.limitsMessage = errorMessage || "Bad server response."; - logger.error("Got bad response retrieving limits:", error); - } else { - this.limitsMessage = "Got an error retrieving limits."; - logger.error("Got some error retrieving limits:", error); - } + logger.log("Converting blob to base64"); + const base64Data = await new Promise<string>((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.onerror = reject; + reader.readAsDataURL(blob); + }); + logger.log("Base64 data length:", base64Data.length); + + logger.log("Converting blob to ArrayBuffer"); + const arrayBuffer = await blob.arrayBuffer(); + logger.log("ArrayBuffer size:", arrayBuffer.byteLength); + + logger.log("Creating and sharing backup"); + await this.createAndShareBackup(base64Data, arrayBuffer, blob); + logger.log("Backup creation and sharing completed"); + } catch (error) { + logger.error("Error during database export:", error); } } + async createAndShareBackup(base64Data: string, arrayBuffer: ArrayBuffer, blob: Blob) { + await DatabaseBackupService.createAndShareBackup(base64Data, arrayBuffer, blob); + } + async uploadImportFile(event: Event) { const target = event.target as HTMLInputElement; if (target.files) { @@ -1942,5 +1955,9 @@ export default class AccountViewView extends Vue { this.userProfileLongitude = updatedProfile.location?.lng || 0; this.includeUserProfileLocation = !!updatedProfile.location; } + + async getDatabase() { + return await db; + } } </script> diff --git a/src/views/ContactGiftingView.vue b/src/views/ContactGiftingView.vue index aa48f796..4c111d4f 100644 --- a/src/views/ContactGiftingView.vue +++ b/src/views/ContactGiftingView.vue @@ -21,7 +21,7 @@ <h2 class="text-base flex gap-4 items-center"> <span class="grow"> <img - src="../assets/blank-square.svg" + src="@/assets/blank-square.svg" width="32" class="inline-block align-middle border border-slate-300 rounded-md mr-1" /> diff --git a/src/views/HelpView.vue b/src/views/HelpView.vue index fe4d9555..0ce0c1e3 100644 --- a/src/views/HelpView.vue +++ b/src/views/HelpView.vue @@ -484,13 +484,13 @@ <a href="http://creativecommons.org/publicdomain/zero/1.0?ref=chooser-v1" target="_blank" rel="license noopener noreferrer"> <span class="text-blue-500 mr-1">CC0 1.0</span> <img - src="../assets/help/creative-commons-circle.svg" + src="@/assets/help/creative-commons-circle.svg" alt="CC circle" width="20" class="display: inline" /> <img - src="../assets/help/creative-commons-zero.svg" + src="@/assets/help/creative-commons-zero.svg" alt="CC zero" width="20" style="display: inline" diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index a685b0e1..9dd159b1 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -135,7 +135,7 @@ Raymer * @version 1.0.0 */ > <li @click="openDialog()"> <img - src="../assets/blank-square.svg" + src="@/assets/blank-square.svg" class="mx-auto border border-blue-500 rounded-md mb-1 cursor-pointer" /> <h3 diff --git a/src/views/ProjectViewView.vue b/src/views/ProjectViewView.vue index 2cad3d86..fe0b078e 100644 --- a/src/views/ProjectViewView.vue +++ b/src/views/ProjectViewView.vue @@ -220,7 +220,7 @@ </li> <li @click="openGiftDialogToProject()"> <img - src="../assets/blank-square.svg" + src="@/assets/blank-square.svg" class="mx-auto border border-blue-300 rounded-md mb-1 cursor-pointer" /> <h3 diff --git a/vite.config.common.mts b/vite.config.common.mts index 1e288f5f..82ae373b 100644 --- a/vite.config.common.mts +++ b/vite.config.common.mts @@ -36,7 +36,15 @@ export async function createBuildConfig(mode: string) { assetsDir: 'assets', chunkSizeWarningLimit: 1000, rollupOptions: { - external: isCapacitor ? ['@capacitor/app'] : [] + external: isCapacitor ? ['@capacitor/app'] : [], + output: { + assetFileNames: (assetInfo) => { + if (assetInfo.name?.endsWith('.svg')) { + return 'assets/[name][extname]'; + } + return 'assets/[name]-[hash][extname]'; + } + } } }, define: { @@ -44,6 +52,8 @@ export async function createBuildConfig(mode: string) { 'process.env.VITE_PLATFORM': JSON.stringify(mode), 'process.env.VITE_PWA_ENABLED': JSON.stringify(!(isElectron || isPyWebView || isCapacitor)), __dirname: isElectron ? JSON.stringify(process.cwd()) : '""', + 'process.env.VITE_ASSET_URL': JSON.stringify(isCapacitor ? './assets/' : '/assets/'), + 'process.env.VITE_BASE_URL': JSON.stringify(isCapacitor ? './' : '/') }, resolve: { alias: { diff --git a/vite.config.web.mts b/vite.config.web.mts index d27befaa..d69a7384 100644 --- a/vite.config.web.mts +++ b/vite.config.web.mts @@ -1,20 +1,92 @@ -import { defineConfig, mergeConfig } from "vite"; +/** + * @file vite.config.web.mts + * @description Vite configuration for web platform builds + * + * This configuration file defines how the application is built for web platforms. + * It extends the base configuration with web-specific settings and optimizations. + * + * Build Process Integration: + * 1. Configuration Loading: + * - Loads environment variables based on build mode + * - Merges base configuration from vite.config.common.mts + * - Loads application-specific configuration + * + * 2. Platform Definition: + * - Sets VITE_PLATFORM environment variable to 'web' + * - Used by PlatformServiceFactory to load web-specific implementations + * + * 3. Build Output: + * - Outputs to 'dist/web' directory + * - Creates vendor chunk for Vue-related dependencies + * - Enables PWA features with auto-update capability + * + * 4. Development vs Production: + * - Development: Enables source maps and development features + * - Production: Optimizes chunks and enables PWA features + * + * Usage: + * - Development: npm run dev + * - Production: npm run build:web + * + * @see vite.config.common.mts + * @see vite.config.utils.mts + * @see PlatformServiceFactory.ts + */ + +import { defineConfig, mergeConfig, loadEnv } from "vite"; import { VitePWA } from "vite-plugin-pwa"; import { createBuildConfig } from "./vite.config.common.mts"; import { loadAppConfig } from "./vite.config.utils.mts"; -export default defineConfig(async () => { +export default defineConfig(async ({ mode }) => { + // Load environment variables based on build mode + const env = loadEnv(mode, process.cwd(), ''); + + // Load base configuration for web platform const baseConfig = await createBuildConfig('web'); + + // Load application-specific configuration const appConfig = await loadAppConfig(); + // Merge configurations with web-specific settings return mergeConfig(baseConfig, { + // Define platform-specific environment variables + define: { + 'import.meta.env.VITE_PLATFORM': JSON.stringify('web'), + }, + + // Build output configuration + build: { + // Output directory for web builds + outDir: 'dist/web', + + // Rollup-specific options + rollupOptions: { + output: { + // Create separate vendor chunk for Vue-related dependencies + manualChunks: { + vendor: ['vue', 'vue-router', 'pinia'], + } + } + } + }, + + // Vite plugins configuration plugins: [ + // Progressive Web App configuration VitePWA({ + // Auto-update service worker registerType: 'autoUpdate', + + // PWA manifest configuration manifest: appConfig.pwaConfig?.manifest, + + // Development options devOptions: { enabled: false }, + + // Workbox configuration for service worker workbox: { cleanupOutdatedCaches: true, skipWaiting: true, diff --git a/vite.config.web.ts b/vite.config.web.ts deleted file mode 100644 index f7376975..00000000 --- a/vite.config.web.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { defineConfig, loadEnv } from "vite"; -import baseConfig from "./vite.config.base"; - -export default defineConfig(({ mode }) => { - const env = loadEnv(mode, process.cwd(), ''); - - return { - ...baseConfig, - define: { - 'import.meta.env.VITE_PLATFORM': JSON.stringify('web'), - }, - build: { - ...baseConfig.build, - outDir: 'dist/web', - rollupOptions: { - ...baseConfig.build.rollupOptions, - output: { - ...baseConfig.build.rollupOptions.output, - manualChunks: { - // Web-specific chunk splitting - vendor: ['vue', 'vue-router', 'pinia'], - } - } - } - } - }; -}); \ No newline at end of file