Compare commits

..

6 Commits

Author SHA1 Message Date
Matthew Raymer
d9ce884513 fix: configure Vite for proper Node.js module handling in Electron
- Add vite-plugin-node-polyfills to provide Node.js built-in module polyfills
- Configure build target as 'node18' for Electron environment
- Switch to CommonJS format for Electron builds
- Add specific polyfills for sqlite3 dependencies (util, stream, buffer)
- Mark Node.js built-in modules as external in Electron builds

This fixes the "Module util has been externalized" error by properly handling
Node.js modules in the Electron environment, particularly for sqlite3 which
depends on Node.js built-in modules.
2025-05-26 14:06:21 +00:00
Matthew Raymer
a1a1543ae1 fix: update component imports in HomeView.vue
- Replace non-existent index.ts import with direct component imports
- Fix ChoiceButtonDialog import to use default import syntax
- Import ImageViewer directly from its component file

This fixes the component loading issues while maintaining the existing functionality.
The remaining linter errors are unrelated to these import changes and should be
addressed separately.
2025-05-26 13:22:59 +00:00
Matt Raymer
93591a5815 docs: storage documentation and feature checklist 2025-05-26 08:43:33 -04:00
Matt Raymer
b30c4c8b30 refactor: migrate database operations to PlatformService
- Add account management methods to PlatformService interface
- Implement account operations in all platform services
- Fix PlatformCapabilities interface by adding sqlite property
- Update util.ts to use PlatformService for account operations
- Standardize account and settings management across platforms

This change improves code organization by:
- Centralizing database operations through PlatformService
- Ensuring consistent account management across platforms
- Making platform-specific implementations more maintainable
- Reducing direct database access in utility functions

Note: Some linter errors remain regarding db.accounts access and sqlite
capabilities that need to be addressed in a follow-up commit.
2025-05-26 06:54:10 -04:00
Matt Raymer
1f9db0ba94 chore:update 2025-05-26 00:33:24 -04:00
Matt Raymer
bdc2d71d3c docs: migrate web storage implementation from wa-sql to absurd-sql
- Update web platform storage solution to use absurd-sql with IndexedDB backend

- Replace wa-sqlite dependencies with absurd-sql and @jlongster/sql.js

- Update WebSQLiteService implementation with SQLiteFS and IndexedDBBackend

- Add performance optimizations (WAL mode, mmap, temp store)

- Add type-safe query method and improved error handling

- Update platform capabilities matrix with new features

- Add absurd-sql compatibility checks in migration service

This change improves transaction support, performance, and reliability of the web platform's SQLite implementation.
2025-05-26 00:32:26 -04:00
213 changed files with 6977 additions and 17204 deletions

172
.cursor/rules/SQLITE.mdc Normal file
View File

@@ -0,0 +1,172 @@
---
description:
globs:
alwaysApply: true
---
# @capacitor-community/sqlite MDC Ruleset
## Project Overview
This ruleset is for the `@capacitor-community/sqlite` plugin, a Capacitor community plugin that provides native and Electron SQLite database functionality with encryption support.
## Key Features
- Native SQLite database support for iOS, Android, and Electron
- Database encryption support using SQLCipher (Native) and better-sqlite3-multiple-ciphers (Electron)
- Biometric authentication support
- Cross-platform database operations
- JSON import/export capabilities
- Database migration support
- Sync table functionality
## Platform Support Matrix
### Core Database Operations
| Operation | Android | iOS | Electron | Web |
|-----------|---------|-----|----------|-----|
| Create Connection (RW) | ✅ | ✅ | ✅ | ✅ |
| Create Connection (RO) | ✅ | ✅ | ✅ | ❌ |
| Open DB (non-encrypted) | ✅ | ✅ | ✅ | ✅ |
| Open DB (encrypted) | ✅ | ✅ | ✅ | ❌ |
| Execute/Query | ✅ | ✅ | ✅ | ✅ |
| Import/Export JSON | ✅ | ✅ | ✅ | ✅ |
### Security Features
| Feature | Android | iOS | Electron | Web |
|---------|---------|-----|----------|-----|
| Encryption | ✅ | ✅ | ✅ | ❌ |
| Biometric Auth | ✅ | ✅ | ✅ | ❌ |
| Secret Management | ✅ | ✅ | ✅ | ❌ |
## Configuration Requirements
### Base Configuration
```typescript
// capacitor.config.ts
{
plugins: {
CapacitorSQLite: {
iosDatabaseLocation: 'Library/CapacitorDatabase',
iosIsEncryption: true,
iosKeychainPrefix: 'your-app-prefix',
androidIsEncryption: true,
electronIsEncryption: true
}
}
}
```
### Platform-Specific Requirements
#### Android
- Minimum SDK: 23
- Target SDK: 35
- Required Gradle JDK: 21
- Required Android Gradle Plugin: 8.7.2
- Required manifest settings for backup prevention
- Required data extraction rules
#### iOS
- No additional configuration needed beyond base setup
- Supports biometric authentication
- Uses keychain for encryption
#### Electron
Required dependencies:
```json
{
"dependencies": {
"better-sqlite3-multiple-ciphers": "latest",
"electron-json-storage": "latest",
"jszip": "latest",
"node-fetch": "2.6.7",
"crypto": "latest",
"crypto-js": "latest"
}
}
```
#### Web
- Requires `sql.js` and `jeep-sqlite`
- Manual copy of `sql-wasm.wasm` to assets folder
- Framework-specific asset placement:
- Angular: `src/assets/`
- Vue/React: `public/assets/`
## Best Practices
### Database Operations
1. Always close connections after use
2. Use transactions for multiple operations
3. Implement proper error handling
4. Use prepared statements for queries
5. Implement proper database versioning
### Security
1. Always use encryption for sensitive data
2. Implement proper secret management
3. Use biometric authentication when available
4. Follow platform-specific security guidelines
### Performance
1. Use appropriate indexes
2. Implement connection pooling
3. Use transactions for bulk operations
4. Implement proper database cleanup
## Common Issues and Solutions
### Android
- Build data properties conflict: Add to `app/build.gradle`:
```gradle
packagingOptions {
exclude 'build-data.properties'
}
```
### Electron
- Node-fetch version must be ≤2.6.7
- For Capacitor Electron v5:
- Use Electron@25.8.4
- Add `"skipLibCheck": true` to tsconfig.json
### Web
- Ensure proper WASM file placement
- Handle browser compatibility
- Implement proper fallbacks
## Version Compatibility
- Requires Node.js ≥16.0.0
- Compatible with Capacitor ≥7.0.0
- Supports TypeScript 4.1.5+
## Testing Requirements
- Unit tests for database operations
- Platform-specific integration tests
- Encryption/decryption tests
- Biometric authentication tests
- Migration tests
- Sync functionality tests
## Documentation
- API Documentation: `/docs/API.md`
- Connection API: `/docs/APIConnection.md`
- DB Connection API: `/docs/APIDBConnection.md`
- Release Notes: `/docs/info_releases.md`
- Changelog: `CHANGELOG.md`
## Contributing Guidelines
- Follow Ionic coding standards
- Use provided linting and formatting tools
- Maintain platform compatibility
- Update documentation
- Add appropriate tests
- Follow semantic versioning
## Maintenance
- Regular security updates
- Platform compatibility checks
- Performance optimization
- Documentation updates
- Dependency updates
## License
MIT License - See LICENSE file for details

View File

@@ -1,6 +0,0 @@
---
description:
globs:
alwaysApply: true
---
use the system date function to understand the proper date and time for all interactions.

View File

@@ -2,12 +2,11 @@
# iOS doesn't like spaces in the app title. # iOS doesn't like spaces in the app title.
TIME_SAFARI_APP_TITLE="TimeSafari_Dev" TIME_SAFARI_APP_TITLE="TimeSafari_Dev"
VITE_APP_SERVER=http://localhost:8080 VITE_APP_SERVER=http://localhost:3000
# This is the claim ID for actions in the BVC project, with the JWT ID on this environment (not production). # This is the claim ID for actions in the BVC project, with the JWT ID on this environment (not production).
VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F
VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000 VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000
# Using shared server by default to ease setup, which works for shared test users. # Using shared server by default to ease setup, which works for shared test users.
VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app
VITE_DEFAULT_PARTNER_API_SERVER=http://localhost:3000 VITE_DEFAULT_PARTNER_API_SERVER=http://localhost:3000
#VITE_DEFAULT_PUSH_SERVER... can't be set up with localhost domain
VITE_PASSKEYS_ENABLED=true VITE_PASSKEYS_ENABLED=true

6
.env.example Normal file
View File

@@ -0,0 +1,6 @@
# Admin DID credentials
ADMIN_DID=did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F
ADMIN_PRIVATE_KEY=2b6472c026ec2aa2c4235c994a63868fc9212d18b58f6cbfe861b52e71330f5b
# API Configuration
ENDORSER_API_URL=https://test-api.endorser.ch/api/v2/claim

View File

@@ -9,4 +9,3 @@ VITE_DEFAULT_ENDORSER_API_SERVER=https://api.endorser.ch
VITE_DEFAULT_IMAGE_API_SERVER=https://image-api.timesafari.app VITE_DEFAULT_IMAGE_API_SERVER=https://image-api.timesafari.app
VITE_DEFAULT_PARTNER_API_SERVER=https://partner-api.endorser.ch VITE_DEFAULT_PARTNER_API_SERVER=https://partner-api.endorser.ch
VITE_DEFAULT_PUSH_SERVER=https://timesafari.app

View File

@@ -9,5 +9,4 @@ VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch
VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app
VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch
VITE_DEFAULT_PUSH_SERVER=https://test.timesafari.app
VITE_PASSKEYS_ENABLED=true VITE_PASSKEYS_ENABLED=true

View File

@@ -4,12 +4,6 @@ module.exports = {
node: true, node: true,
es2022: true, es2022: true,
}, },
ignorePatterns: [
'node_modules/',
'dist/',
'dist-electron/',
'*.d.ts'
],
extends: [ extends: [
"plugin:vue/vue3-recommended", "plugin:vue/vue3-recommended",
"eslint:recommended", "eslint:recommended",

5
.gitignore vendored
View File

@@ -51,7 +51,6 @@ vendor/
# Build logs # Build logs
build_logs/ build_logs/
# PWA icon files generated by capacitor-assets android/app/src/main/assets/public
icons android/app/src/main/res

View File

@@ -9,6 +9,19 @@ For a quick dev environment setup, use [pkgx](https://pkgx.dev).
- Node.js (LTS version recommended) - Node.js (LTS version recommended)
- npm (comes with Node.js) - npm (comes with Node.js)
- Git - Git
- For Android builds: Android Studio with SDK installed
- For iOS builds: macOS with Xcode and ruby gems & bundle
- `pkgx +rubygems.org sh`
- ... and you may have to fix these, especially with pkgx
```bash
gem_path=$(which gem)
shortened_path="${gem_path:h:h}"
export GEM_HOME=$shortened_path
export GEM_PATH=$shortened_path
```
- For desktop builds: Additional build tools based on your OS - For desktop builds: Additional build tools based on your OS
## Forks ## Forks
@@ -71,7 +84,7 @@ Install dependencies:
* For test, build the app (because test server is not yet set up to build): * For test, build the app (because test server is not yet set up to build):
```bash ```bash
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_DEFAULT_PUSH_SERVER=https://test.timesafari.app VITE_PASSKEYS_ENABLED=true npm run build TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_PASSKEYS_ENABLED=true npm run build
``` ```
... and transfer to the test server: ... and transfer to the test server:
@@ -313,32 +326,6 @@ npm run build:electron-prod && npm run electron:start
Prerequisites: macOS with Xcode installed Prerequisites: macOS with Xcode installed
#### First-time iOS Configuration
- Generate certificates inside XCode.
- Right-click on App and under Signing & Capabilities set the Team.
#### Each Release
0. First time (or if XCode dependencies change):
- `pkgx +rubygems.org sh`
- ... and you may have to fix these, especially with pkgx
```bash
gem_path=$(which gem)
shortened_path="${gem_path:h:h}"
export GEM_HOME=$shortened_path
export GEM_PATH=$shortened_path
```
```bash
cd ios/App
pod install
```
1. Build the web assets: 1. Build the web assets:
```bash ```bash
@@ -347,7 +334,6 @@ Prerequisites: macOS with Xcode installed
npm run build:capacitor npm run build:capacitor
``` ```
2. Update iOS project with latest build: 2. Update iOS project with latest build:
```bash ```bash
@@ -359,11 +345,7 @@ Prerequisites: macOS with Xcode installed
3. Copy the assets: 3. Copy the assets:
```bash ```bash
# It makes no sense why capacitor-assets will not run without these but it actually changes the contents.
mkdir -p ios/App/App/Assets.xcassets/AppIcon.appiconset mkdir -p ios/App/App/Assets.xcassets/AppIcon.appiconset
echo '{"images":[]}' > ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json
mkdir -p ios/App/App/Assets.xcassets/Splash.imageset
echo '{"images":[]}' > ios/App/App/Assets.xcassets/Splash.imageset/Contents.json
npx capacitor-assets generate --ios npx capacitor-assets generate --ios
``` ```
@@ -371,10 +353,10 @@ Prerequisites: macOS with Xcode installed
``` ```
cd ios/App cd ios/App
xcrun agvtool new-version 25 xcrun agvtool new-version 15
# Unfortunately this edits Info.plist directly. # Unfortunately this edits Info.plist directly.
#xcrun agvtool new-marketing-version 0.4.5 #xcrun agvtool new-marketing-version 0.4.5
cat App.xcodeproj/project.pbxproj | sed "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 0.5.1;/g" > temp cat App.xcodeproj/project.pbxproj | sed "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 0.4.5;/g" > temp
mv temp App.xcodeproj/project.pbxproj mv temp App.xcodeproj/project.pbxproj
cd - cd -
``` ```
@@ -387,25 +369,28 @@ Prerequisites: macOS with Xcode installed
6. Use Xcode to build and run on simulator or device. 6. Use Xcode to build and run on simulator or device.
* Select Product -> Destination with some Simulator version. Then click the run arrow.
7. Release 7. Release
* Someday: Under "General" we want to rename a bunch of things to "Time Safari" * Under "General" renamed a bunch of things to "Time Safari"
* Choose Product -> Destination -> Any iOS Device * Choose Product -> Destination -> Build Any iOS
* Choose Product -> Archive * Choose Product -> Archive
* This will trigger a build and take time, needing user's "login" keychain password (user's login password), repeatedly. * This will trigger a build and take time, needing user's "login" keychain password which is just their login password, repeatedly.
* If it fails with `building for 'iOS', but linking in dylib (.../.pkgx/zlib.net/v1.3.0/lib/libz.1.3.dylib) built for 'macOS'` then run XCode outside that terminal (ie. not with `npx cap open ios`). * If it fails with `building for 'iOS', but linking in dylib (.../.pkgx/zlib.net/v1.3.0/lib/libz.1.3.dylib) built for 'macOS'` then run XCode outside that terminal (ie. not with `npx cap open ios`).
* Click Distribute -> App Store Connect * Click Distribute -> App Store Connect
* In AppStoreConnect, add the build to the distribution: remove the current build with the "-" when you hover over it, then "Add Build" with the new build. * In AppStoreConnect, add the build to the distribution: remove the current build with the "-" when you hover over it, then "Add Build" with the new build.
* May have to go to App Review, click Submission, then hover over the build and click "-".
* It can take 15 minutes for the build to show up in the list of builds. * It can take 15 minutes for the build to show up in the list of builds.
* You'll probably have to "Manage" something about encryption, disallowed in France. * You'll probably have to "Manage" something about encryption, disallowed in France.
* Then "Save" and "Add to Review" and "Resubmit to App Review". * Then "Save" and "Add to Review" and "Resubmit to App Review".
#### First-time iOS Configuration
- Generate certificates inside XCode.
- Right-click on App and under Signing & Capabilities set the Team.
### Android Build ### Android Build
Prerequisites: Android Studio with Java SDK installed Prerequisites: Android Studio with SDK installed
1. Build the web assets: 1. Build the web assets:
@@ -460,9 +445,7 @@ Prerequisites: Android Studio with Java SDK installed
* Then `bundleRelease`: * Then `bundleRelease`:
```bash ```bash
cd android
./gradlew bundleRelease -Dlint.baselines.continue=true ./gradlew bundleRelease -Dlint.baselines.continue=true
cd -
``` ```
... and find your `aab` file at app/build/outputs/bundle/release ... and find your `aab` file at app/build/outputs/bundle/release
@@ -475,8 +458,6 @@ At play.google.com/console:
- Hit "Next". - Hit "Next".
- Save, go to "Publishing Overview" as prompted, and click "Send changes for review". - Save, go to "Publishing Overview" as prompted, and click "Send changes for review".
- Note that if you add testers, you have to go to "Publishing Overview" and send those changes or your (closed) testers won't see it.
## First-time Android Configuration for deep links ## First-time Android Configuration for deep links

View File

@@ -1,538 +0,0 @@
# CEFPython Implementation Survey for TimeSafari
**Author:** Matthew Raymer
**Date:** December 2025
**Project:** TimeSafari Cross-Platform Desktop Implementation
## Executive Summary
This survey evaluates implementing CEFPython as an additional desktop platform for TimeSafari, with full integration into the existing migration system used by Capacitor and native web platforms.
### Key Findings
**Feasibility:****Highly Feasible** - CEFPython can integrate seamlessly with TimeSafari's existing architecture
**Migration System Compatibility:****Full Compatibility** - Can use the exact same `migration.ts` system as Capacitor and web
**Performance:****Excellent** - Native Python backend with Chromium rendering engine
**Security:****Strong** - Chromium's security model with Python backend isolation
---
## 1. Architecture Overview
### 1.1 Current Platform Architecture
TimeSafari uses a sophisticated cross-platform architecture with shared codebase and platform-specific implementations:
```typescript
src/
main.common.ts # Shared initialization
main.web.ts # Web/PWA entry point
main.capacitor.ts # Mobile entry point
main.electron.ts # Electron entry point
main.pywebview.ts # PyWebView entry point
main.cefpython.ts # NEW: CEFPython entry point
services/
PlatformService.ts # Platform abstraction interface
PlatformServiceFactory.ts
platforms/
WebPlatformService.ts
CapacitorPlatformService.ts
ElectronPlatformService.ts
PyWebViewPlatformService.ts
CEFPythonPlatformService.ts # NEW
cefpython/ # NEW: CEFPython backend
main.py
handlers/
database.py # SQLite with migration support
crypto.py # Cryptographic operations
api.py # API server integration
bridge/
javascript_bridge.py # JS-Python communication
```
### 1.2 Migration System Integration
**Key Insight:** CEFPython can use the exact same migration system as Capacitor and web platforms:
```typescript
// src/main.cefpython.ts - CEFPython entry point
import { initializeApp } from "./main.common";
import { runMigrations } from "./db-sql/migration";
import { CEFPythonPlatformService } from "./services/platforms/CEFPythonPlatformService";
const app = initializeApp();
// Initialize CEFPython platform service
const platformService = new CEFPythonPlatformService();
// Run migrations using the same system as Capacitor
async function initializeDatabase() {
const sqlExec = (sql: string) => platformService.dbExecute(sql);
const sqlQuery = (sql: string) => platformService.dbQuery(sql);
const extractMigrationNames = (result: any) => {
const names = result.values?.map((row: any) => row.name) || [];
return new Set(names);
};
await runMigrations(sqlExec, sqlQuery, extractMigrationNames);
}
// Initialize database before mounting app
initializeDatabase().then(() => {
app.mount("#app");
});
```
---
## 2. Python Backend Implementation
### 2.1 Database Handler with Migration Support
```python
# src/cefpython/handlers/database.py
import sqlite3
from pathlib import Path
from typing import List, Dict, Any
class DatabaseHandler:
def __init__(self):
self.db_path = self._get_db_path()
self.connection = sqlite3.connect(str(self.db_path))
self.connection.row_factory = sqlite3.Row
# Configure for better performance
self.connection.execute("PRAGMA journal_mode=WAL;")
self.connection.execute("PRAGMA synchronous=NORMAL;")
def query(self, sql: str, params: List[Any] = None) -> Dict[str, Any]:
"""Execute SQL query and return results in Capacitor-compatible format"""
cursor = self.connection.cursor()
if params:
cursor.execute(sql, params)
else:
cursor.execute(sql)
if sql.strip().upper().startswith('SELECT'):
columns = [description[0] for description in cursor.description]
rows = []
for row in cursor.fetchall():
rows.append(dict(zip(columns, row)))
return {'values': rows} # Match Capacitor format
else:
self.connection.commit()
return {'affected_rows': cursor.rowcount}
def execute(self, sql: string, params: List[Any] = None) -> Dict[str, Any]:
"""Execute SQL statement (for INSERT, UPDATE, DELETE, CREATE)"""
cursor = self.connection.cursor()
if params:
cursor.execute(sql, params)
else:
cursor.execute(sql)
self.connection.commit()
return {
'changes': {
'changes': cursor.rowcount,
'lastId': cursor.lastrowid
}
}
```
### 2.2 Platform Service Implementation
```typescript
// src/services/platforms/CEFPythonPlatformService.ts
import { PlatformService } from '../PlatformService';
import { runMigrations } from '@/db-sql/migration';
export class CEFPythonPlatformService implements PlatformService {
private bridge: any;
constructor() {
this.bridge = (window as any).cefBridge;
if (!this.bridge) {
throw new Error('CEFPython bridge not available');
}
}
// Database operations using the same interface as Capacitor
async dbQuery(sql: string, params?: any[]): Promise<any> {
const result = await this.bridge.call('database', 'query', sql, params || []);
return result;
}
async dbExecute(sql: string, params?: any[]): Promise<any> {
const result = await this.bridge.call('database', 'execute', sql, params || []);
return result;
}
// Migration system integration
async runMigrations(): Promise<void> {
const sqlExec: (sql: string) => Promise<any> = this.dbExecute.bind(this);
const sqlQuery: (sql: string) => Promise<any> = this.dbQuery.bind(this);
const extractMigrationNames: (result: any) => Set<string> = (result) => {
const names = result.values?.map((row: any) => row.name) || [];
return new Set(names);
};
await runMigrations(sqlExec, sqlQuery, extractMigrationNames);
}
// Platform detection
isCEFPython(): boolean {
return true;
}
getCapabilities(): PlatformCapabilities {
return {
hasCamera: true,
hasFileSystem: true,
hasNotifications: true,
hasSQLite: true,
hasCrypto: true
};
}
}
```
---
## 3. Migration System Compatibility
### 3.1 Key Advantage
**CEFPython can use the exact same migration system as Capacitor:**
```typescript
// Both Capacitor and CEFPython use the same migration.ts
import { runMigrations } from '@/db-sql/migration';
// Capacitor implementation
const sqlExec: (sql: string) => Promise<capSQLiteChanges> = this.db.execute.bind(this.db);
const sqlQuery: (sql: string) => Promise<DBSQLiteValues> = this.db.query.bind(this.db);
// CEFPython implementation
const sqlExec: (sql: string) => Promise<any> = this.dbExecute.bind(this);
const sqlQuery: (sql: string) => Promise<any> = this.dbQuery.bind(this);
// Both use the same migration runner
await runMigrations(sqlExec, sqlQuery, extractMigrationNames);
```
### 3.2 Database Format Compatibility
The Python database handler returns data in the same format as Capacitor:
```python
# Python returns Capacitor-compatible format
def query(self, sql: str, params: List[Any] = None) -> Dict[str, Any]:
# ... execute query ...
return {
'values': [
{'name': '001_initial', 'executed_at': '2025-01-01'},
{'name': '002_add_contacts', 'executed_at': '2025-01-02'}
]
}
```
This matches the Capacitor format:
```typescript
// Capacitor returns same format
const result = await this.db.query("SELECT name FROM migrations");
// result = { values: [{ name: '001_initial' }, { name: '002_add_contacts' }] }
```
---
## 4. Build Configuration
### 4.1 Vite Configuration
```typescript
// vite.config.cefpython.mts
import { defineConfig } from 'vite';
import { createBuildConfig } from './vite.config.common.mts';
export default defineConfig({
...createBuildConfig('cefpython'),
define: {
'process.env.VITE_PLATFORM': JSON.stringify('cefpython'),
'process.env.VITE_PWA_ENABLED': JSON.stringify(false),
__IS_MOBILE__: JSON.stringify(false),
__USE_QR_READER__: JSON.stringify(true)
}
});
```
### 4.2 Package.json Scripts
```json
{
"scripts": {
"build:cefpython": "vite build --config vite.config.cefpython.mts",
"dev:cefpython": "concurrently \"npm run dev:web\" \"python src/cefpython/main.py --dev\"",
"test:cefpython": "python -m pytest tests/cefpython/"
}
}
```
### 4.3 Python Requirements
```txt
# requirements-cefpython.txt
cefpython3>=66.1
cryptography>=3.4.0
requests>=2.25.0
pyinstaller>=4.0
pytest>=6.0.0
```
---
## 5. Platform Service Factory Integration
### 5.1 Updated Factory
```typescript
// src/services/PlatformServiceFactory.ts
import { CEFPythonPlatformService } from './platforms/CEFPythonPlatformService';
export function createPlatformService(platform: string): PlatformService {
switch (platform) {
case 'web':
return new WebPlatformService();
case 'capacitor':
return new CapacitorPlatformService();
case 'electron':
return new ElectronPlatformService();
case 'pywebview':
return new PyWebViewPlatformService();
case 'cefpython':
return new CEFPythonPlatformService(); // NEW
default:
throw new Error(`Unsupported platform: ${platform}`);
}
}
```
---
## 6. Performance and Security Analysis
### 6.1 Performance Comparison
| Metric | Electron | PyWebView | CEFPython | Notes |
|--------|----------|-----------|-----------|-------|
| **Memory Usage** | 150-200MB | 80-120MB | 100-150MB | CEFPython more efficient than Electron |
| **Startup Time** | 3-5s | 2-3s | 2-4s | Similar to PyWebView |
| **Database Performance** | Good | Good | Excellent | Native SQLite |
| **Crypto Performance** | Good | Good | Excellent | Native Python crypto |
| **Bundle Size** | 120-150MB | 50-80MB | 80-120MB | Smaller than Electron |
### 6.2 Security Features
```python
# src/cefpython/utils/security.py
class SecurityManager:
def __init__(self):
self.blocked_domains = set(['malicious-site.com'])
self.allowed_schemes = {'https', 'http', 'file'}
def validate_network_access(self, url: str) -> bool:
"""Validate if network access is allowed"""
from urllib.parse import urlparse
parsed = urlparse(url)
# Check blocked domains
if parsed.hostname in self.blocked_domains:
return False
# Allow HTTPS only for external domains
if parsed.scheme != 'https' and parsed.hostname != 'localhost':
return False
return True
```
---
## 7. Migration Strategy
### 7.1 Phase 1: Foundation (Week 1-2)
**Objectives:**
- Set up CEFPython development environment
- Create basic application structure
- Implement database handler with migration support
- Establish JavaScript-Python bridge
**Deliverables:**
- [ ] Basic CEFPython application that loads TimeSafari web app
- [ ] Database handler with SQLite integration
- [ ] Migration system integration
- [ ] JavaScript bridge for communication
### 7.2 Phase 2: Platform Integration (Week 3-4)
**Objectives:**
- Implement CEFPython platform service
- Integrate with existing migration system
- Test database operations with real data
- Validate migration compatibility
**Deliverables:**
- [ ] CEFPython platform service implementation
- [ ] Migration system integration
- [ ] Database compatibility testing
- [ ] Performance benchmarking
### 7.3 Phase 3: Feature Integration (Week 5-6)
**Objectives:**
- Integrate with existing platform features
- Implement API server integration
- Add security features
- Test with real user workflows
**Deliverables:**
- [ ] Full feature compatibility
- [ ] API integration
- [ ] Security implementation
- [ ] User workflow testing
### 7.4 Phase 4: Polish and Distribution (Week 7-8)
**Objectives:**
- Optimize performance
- Add build and distribution scripts
- Create documentation
- Prepare for release
**Deliverables:**
- [ ] Performance optimization
- [ ] Build automation
- [ ] Documentation
- [ ] Release-ready application
---
## 8. Risk Assessment
### 8.1 Technical Risks
| Risk | Probability | Impact | Mitigation |
|------|-------------|--------|------------|
| **CEFPython compatibility issues** | Medium | High | Use stable CEFPython version, test thoroughly |
| **Migration system integration** | Low | High | Follow existing patterns, extensive testing |
| **Performance issues** | Low | Medium | Benchmark early, optimize as needed |
| **Security vulnerabilities** | Low | High | Implement security manager, regular audits |
### 8.2 Development Risks
| Risk | Probability | Impact | Mitigation |
|------|-------------|--------|------------|
| **Python/CEF knowledge gap** | Medium | Medium | Training, documentation, pair programming |
| **Integration complexity** | Medium | Medium | Incremental development, extensive testing |
| **Build system complexity** | Low | Medium | Automated build scripts, CI/CD |
---
## 9. Success Metrics
### 9.1 Technical Metrics
- [ ] **Migration Compatibility:** 100% compatibility with existing migration system
- [ ] **Performance:** < 150MB memory usage, < 4s startup time
- [ ] **Security:** Pass security audit, no critical vulnerabilities
- [ ] **Reliability:** 99%+ uptime, < 1% error rate
### 9.2 Development Metrics
- [ ] **Code Quality:** 90%+ test coverage, < 5% code duplication
- [ ] **Documentation:** Complete API documentation, user guides
- [ ] **Build Automation:** Automated builds, CI/CD pipeline
- [ ] **Release Readiness:** Production-ready application
---
## 10. Conclusion
### 10.1 Recommendation
**✅ PROCEED WITH IMPLEMENTATION**
CEFPython provides an excellent opportunity to add a robust desktop platform to TimeSafari with:
1. **Full Migration System Compatibility:** Can use the exact same migration system as Capacitor and web
2. **Native Performance:** Python backend with Chromium rendering
3. **Security:** Chromium's security model with Python backend isolation
4. **Development Efficiency:** Follows established patterns and architecture
### 10.2 Implementation Priority
**High Priority:**
- Database handler with migration support
- JavaScript-Python bridge
- Platform service integration
- Basic application structure
**Medium Priority:**
- Crypto handler integration
- API server integration
- Security features
- Performance optimization
**Low Priority:**
- Advanced features
- Build automation
- Documentation
- Distribution packaging
### 10.3 Timeline
**Total Duration:** 8 weeks (2 months)
**Team Size:** 1-2 developers
**Risk Level:** Medium
**Confidence:** 85%
The implementation leverages TimeSafari's existing architecture and migration system, making it a natural addition to the platform ecosystem while providing users with a high-performance desktop option.
---
## 11. Next Steps
1. **Immediate Actions:**
- Set up development environment
- Create basic CEFPython application structure
- Implement database handler with migration support
2. **Week 1-2:**
- Complete foundation implementation
- Test migration system integration
- Validate database operations
3. **Week 3-4:**
- Implement platform service
- Integrate with existing features
- Begin performance testing
4. **Week 5-6:**
- Complete feature integration
- Security implementation
- User workflow testing
5. **Week 7-8:**
- Performance optimization
- Build automation
- Documentation and release preparation
This implementation will provide TimeSafari users with a robust, secure, and high-performance desktop application that seamlessly integrates with the existing ecosystem.

View File

@@ -7,13 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [0.4.7]
### Fixed
- Cameras everywhere
### Changed
- IndexedDB -> SQLite
## [0.4.5] - 2025.02.23 ## [0.4.5] - 2025.02.23
### Added ### Added
- Total amounts of gives on project page - Total amounts of gives on project page

File diff suppressed because it is too large Load Diff

View File

@@ -1,469 +0,0 @@
# GiftedDialog Component Decomposition Plan
## Overview
This document outlines a comprehensive plan to refactor the GiftedDialog component by breaking it into smaller, more manageable sub-components. This approach will improve maintainability, testability, and reusability while preparing the codebase for future Pinia integration.
## Current State Analysis
The GiftedDialog component (1060 lines) is a complex Vue component that handles:
- **Two-step wizard UI**: Entity selection → Gift details
- **Multiple entity types**: Person/Project as giver/recipient
- **Complex conditional rendering**: Based on context and entity types
- **Form validation and submission**: Gift recording with API integration
- **State management**: UI flow, entity selection, form data
### Key Challenges
1. **Large single file**: Difficult to navigate and maintain
2. **Mixed concerns**: UI logic, business logic, and API calls in one place
3. **Complex state**: Multiple interconnected reactive properties
4. **Testing difficulty**: Hard to test individual features in isolation
5. **Reusability**: Components like entity grids could be reused elsewhere
## Decomposition Strategy
### Phase 1: Extract Display Components (✅ COMPLETED)
These components handle pure presentation with minimal business logic:
#### 1. PersonCard.vue ✅
- **Purpose**: Display individual person entities with selection capability
- **Features**:
- Person avatar using EntityIcon
- Selection states (selectable, conflicted, disabled)
- Time icon overlay for contacts
- Click event handling
- **Props**: `person`, `selectable`, `conflicted`, `showTimeIcon`
- **Emits**: `person-selected`
#### 2. ProjectCard.vue ✅
- **Purpose**: Display individual project entities with selection capability
- **Features**:
- Project icon using ProjectIcon
- Project name and issuer information
- Click event handling
- **Props**: `project`, `activeDid`, `allMyDids`, `allContacts`
- **Emits**: `project-selected`
#### 3. EntitySummaryButton.vue ✅
- **Purpose**: Display selected entity with edit capability in step 2
- **Features**:
- Entity avatar (person or project)
- Entity name and role label
- Editable vs locked states
- Edit button functionality
- **Props**: `entity`, `entityType`, `label`, `editable`
- **Emits**: `edit-requested`
#### 4. AmountInput.vue ✅
- **Purpose**: Specialized numeric input with increment/decrement controls
- **Features**:
- Increment/decrement buttons with validation
- Configurable min/max values and step size
- Input validation and formatting
- v-model compatibility
- **Props**: `value`, `min`, `max`, `step`, `inputId`
- **Emits**: `update:value`
### Phase 2: Extract Layout Components (✅ COMPLETED)
These components handle layout and entity organization:
#### 5. EntityGrid.vue ✅
- **Purpose**: Unified grid layout for displaying people or projects
- **Features**:
- Responsive grid layout for people/projects
- Special entity integration (You, Unnamed)
- Conflict detection integration
- Empty state messaging
- Show All navigation
- Event delegation for entity selection
- **Props**: `entityType`, `entities`, `maxItems`, `activeDid`, `allMyDids`, `allContacts`, `conflictChecker`, `showYouEntity`, `youSelectable`, `showAllRoute`, `showAllQueryParams`
- **Emits**: `entity-selected`
#### 6. SpecialEntityCard.vue ✅
- **Purpose**: Handle special entities like "You" and "Unnamed"
- **Features**:
- Special icon display (hand, question mark)
- Conflict state handling
- Configurable styling based on entity type
- Click event handling
- **Props**: `entityType`, `label`, `icon`, `selectable`, `conflicted`, `entityData`
- **Emits**: `entity-selected`
#### 7. ShowAllCard.vue ✅
- **Purpose**: Handle "Show All" navigation functionality
- **Features**:
- Router-link integration
- Query parameter passing
- Consistent visual styling
- Hover effects
- **Props**: `entityType`, `routeName`, `queryParams`
- **Emits**: None (uses router-link)
### Phase 3: Extract Step Components (✅ COMPLETED)
These components handle major UI sections:
#### 8. EntitySelectionStep.vue ✅
- **Purpose**: Complete step 1 entity selection interface
- **Features**:
- Dynamic step labeling based on context
- EntityGrid integration for unified entity display
- Conflict detection and prevention
- Special entity handling (You, Unnamed)
- Show All navigation with context preservation
- Cancel functionality
- Event delegation for entity selection
- **Props**: `stepType`, `giverEntityType`, `recipientEntityType`, `showProjects`, `isFromProjectView`, `projects`, `allContacts`, `activeDid`, `allMyDids`, `conflictChecker`, `fromProjectId`, `toProjectId`, `giver`, `receiver`
- **Emits**: `entity-selected`, `cancel`
#### 9. GiftDetailsStep.vue ✅
- **Purpose**: Complete step 2 gift details form interface
- **Features**:
- Entity summary display with edit capability
- Gift description input with placeholder support
- Amount input with increment/decrement controls
- Unit code selection (HUR, USD, BTC, etc.)
- Photo & more options navigation
- Conflict detection and warning display
- Form validation and submission
- Cancel functionality
- **Props**: `giver`, `receiver`, `giverEntityType`, `recipientEntityType`, `description`, `amount`, `unitCode`, `prompt`, `isFromProjectView`, `hasConflict`, `offerId`, `fromProjectId`, `toProjectId`
- **Emits**: `update:description`, `update:amount`, `update:unitCode`, `edit-entity`, `explain-data`, `submit`, `cancel`
### Phase 4: Refactor Main Component (FINAL)
#### 9. GiftedDialog.vue (PLANNED REFACTOR)
- **Purpose**: Orchestrate sub-components and manage overall state
- **Responsibilities**:
- Step navigation logic
- Entity conflict detection
- API integration for gift recording
- Success/error handling
- Dialog visibility management
## Implementation Progress
### ✅ Completed Components
**Phase 1: Display Components**
1. **PersonCard.vue** - Individual person display with selection
2. **ProjectCard.vue** - Individual project display with selection
3. **EntitySummaryButton.vue** - Selected entity display with edit capability
4. **AmountInput.vue** - Numeric input with increment/decrement controls
**Phase 2: Layout Components**
5. **EntityGrid.vue** - Unified grid layout for entity selection
6. **SpecialEntityCard.vue** - Special entities (You, Unnamed) with conflict handling
7. **ShowAllCard.vue** - Show All navigation with router integration
**Phase 3: Step Components**
8. **EntitySelectionStep.vue** - Complete step 1 entity selection interface
9. **GiftDetailsStep.vue** - Complete step 2 gift details form interface
### 🔄 Next Steps
1. **Update GiftedDialog.vue** - Integrate all Phase 1-3 components
2. **Test integration** - Ensure functionality remains intact
3. **Create unit tests** - For all new components
4. **Performance validation** - Ensure no regression
5. **Phase 4 planning** - Refactor main component to orchestration only
### 📋 Future Phases
1. **Extract EntitySelectionStep.vue** - Complete step 1 logic
2. **Extract GiftDetailsStep.vue** - Complete step 2 logic
3. **Refactor main component** - Minimal orchestration logic
4. **Add comprehensive tests** - Unit tests for each component
5. **Prepare for Pinia** - State management migration
## Benefits of This Approach
### 1. Incremental Refactoring
- Each phase can be implemented and tested independently
- Reduces risk of breaking existing functionality
- Allows for gradual improvement over time
### 2. Improved Maintainability
- Smaller, focused components are easier to understand
- Clear separation of concerns
- Easier to locate and fix bugs
### 3. Enhanced Testability
- Individual components can be unit tested in isolation
- Easier to mock dependencies
- Better test coverage possible
### 4. Better Reusability
- Components like EntityGrid can be used in other views
- PersonCard and ProjectCard can be used throughout the app
- AmountInput can be reused for other numeric inputs
### 5. Pinia Preparation
- Smaller components make state management migration easier
- Clear data flow patterns emerge
- Easier to identify what state should be global vs local
## Integration Examples
### Using PersonCard in EntityGrid
```vue
<template>
<ul class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-2 gap-y-4">
<PersonCard
v-for="person in people"
:key="person.did"
:person="person"
:conflicted="wouldCreateConflict(person.did)"
@person-selected="handlePersonSelected"
/>
</ul>
</template>
```
### Using AmountInput in GiftDetailsStep
```vue
<template>
<AmountInput
:value="amount"
:min="0"
:max="1000"
input-id="gift-amount"
@update:value="amount = $event"
/>
</template>
```
### Using EntitySummaryButton in GiftDetailsStep
```vue
<template>
<div class="grid grid-cols-2 gap-2">
<EntitySummaryButton
:entity="giver"
entity-type="person"
label="Received from:"
:editable="canEditGiver"
@edit-requested="handleEditGiver"
/>
<EntitySummaryButton
:entity="receiver"
entity-type="person"
label="Given to:"
:editable="canEditReceiver"
@edit-requested="handleEditReceiver"
/>
</div>
</template>
```
### Using EntityGrid in EntitySelectionStep
```vue
<template>
<div>
<label class="block font-bold mb-4">
{{ stepLabel }}
</label>
<EntityGrid
:entity-type="shouldShowProjects ? 'projects' : 'people'"
:entities="shouldShowProjects ? projects : allContacts"
:max-items="10"
:active-did="activeDid"
:all-my-dids="allMyDids"
:all-contacts="allContacts"
:conflict-checker="wouldCreateConflict"
:show-you-entity="showYouEntity"
:you-selectable="youSelectable"
:show-all-route="showAllRoute"
:show-all-query-params="showAllQueryParams"
@entity-selected="handleEntitySelected"
/>
</div>
</template>
```
### Using SpecialEntityCard Standalone
```vue
<template>
<ul class="grid grid-cols-4 gap-2">
<SpecialEntityCard
entity-type="you"
label="You"
icon="hand"
:conflicted="wouldCreateConflict(activeDid)"
:entity-data="{ did: activeDid, name: 'You' }"
@entity-selected="handleYouSelected"
/>
<SpecialEntityCard
entity-type="unnamed"
label="Unnamed"
icon="circle-question"
:entity-data="{ did: '', name: 'Unnamed' }"
@entity-selected="handleUnnamedSelected"
/>
</ul>
</template>
```
### Using EntitySelectionStep in GiftedDialog
```vue
<template>
<div v-show="currentStep === 1">
<EntitySelectionStep
:step-type="stepType"
:giver-entity-type="giverEntityType"
:recipient-entity-type="recipientEntityType"
:show-projects="showProjects"
:is-from-project-view="isFromProjectView"
:projects="projects"
:all-contacts="allContacts"
:active-did="activeDid"
:all-my-dids="allMyDids"
:conflict-checker="wouldCreateConflict"
:from-project-id="fromProjectId"
:to-project-id="toProjectId"
:giver="giver"
:receiver="receiver"
@entity-selected="handleEntitySelected"
@cancel="cancel"
/>
</div>
</template>
```
### Using GiftDetailsStep in GiftedDialog
```vue
<template>
<div v-show="currentStep === 2">
<GiftDetailsStep
:giver="giver"
:receiver="receiver"
:giver-entity-type="giverEntityType"
:recipient-entity-type="recipientEntityType"
:description="description"
:amount="parseFloat(amountInput)"
:unit-code="unitCode"
:prompt="prompt"
:is-from-project-view="isFromProjectView"
:has-conflict="hasPersonConflict"
:offer-id="offerId"
:from-project-id="fromProjectId"
:to-project-id="toProjectId"
@update:description="description = $event"
@update:amount="amountInput = $event.toString()"
@update:unit-code="unitCode = $event"
@edit-entity="handleEditEntity"
@explain-data="explainData"
@submit="handleSubmit"
@cancel="cancel"
/>
</div>
</template>
```
## Migration Strategy
### Backward Compatibility
- Maintain existing API and prop interfaces
- Ensure all existing functionality works unchanged
- Preserve all event emissions and callbacks
### Testing Strategy
- Create unit tests for each new component
- Maintain existing integration tests
- Add visual regression tests for UI components
### Performance Considerations
- Monitor bundle size impact
- Ensure no performance regression
- Optimize component loading if needed
## Security Considerations
### Input Validation
- AmountInput includes proper numeric validation
- All user inputs are validated before processing
- XSS prevention through proper Vue templating
### Data Handling
- No sensitive data stored in component state
- Proper prop validation and type checking
- Secure API communication maintained
## Conclusion
This decomposition plan provides a structured approach to refactoring the GiftedDialog component while maintaining functionality and preparing for future enhancements. The incremental approach reduces risk and allows for continuous improvement of the codebase.
The completed Phase 1 components (PersonCard, ProjectCard, EntitySummaryButton, AmountInput) provide a solid foundation for the remaining phases and demonstrate the benefits of component decomposition in terms of maintainability, testability, and reusability.
---
## Final Integration Results
### ✅ **INTEGRATION COMPLETE**
**Completed on**: 2025-01-28
**Results:**
- **Main GiftedDialog template**: Reduced from ~200 lines to ~20 lines
- **Components created**: 9 focused, reusable components
- **Lines of code**: ~2,000 lines of well-structured component code
- **Backward compatibility**: 100% maintained
- **Build status**: ✅ Passing
- **Runtime status**: ✅ Working
**Components Successfully Integrated:**
1. **PersonCard.vue** - Individual person display with conflict detection
2. **ProjectCard.vue** - Individual project display with issuer info
3. **EntitySummaryButton.vue** - Selected entity display with edit capability
4. **AmountInput.vue** - Numeric input with validation and controls
5. **SpecialEntityCard.vue** - "You" and "Unnamed" entity handling
6. **ShowAllCard.vue** - Navigation with router integration
7. **EntityGrid.vue** - Unified grid layout orchestration
8. **EntitySelectionStep.vue** - Complete Step 1 interface
9. **GiftDetailsStep.vue** - Complete Step 2 interface
**Integration Benefits Achieved:**
-**Maintainability**: Each component has single responsibility
-**Testability**: Components can be unit tested in isolation
-**Reusability**: Components can be used across the application
-**Readability**: Clear separation of concerns and focused logic
-**Debugging**: Easier to identify and fix issues
-**Performance**: No performance regression, improved code splitting
**Next Steps:**
1. **Pinia State Management**: Ready for state management migration
2. **Component Testing**: Add comprehensive unit tests
3. **Visual Testing**: Add Playwright component tests
4. **Documentation**: Update component documentation
5. **Optimization**: Fine-tune performance if needed
---
**Author**: Matthew Raymer
**Last Updated**: 2025-01-28
**Status**: ✅ **INTEGRATION COMPLETE - READY FOR PRODUCTION**

File diff suppressed because it is too large Load Diff

View File

@@ -31,8 +31,8 @@ android {
applicationId "app.timesafari.app" applicationId "app.timesafari.app"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 26 versionCode 18
versionName "0.5.1" versionName "0.4.7"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions { aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
@@ -91,8 +91,6 @@ dependencies {
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion" implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion" implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
implementation project(':capacitor-android') implementation project(':capacitor-android')
implementation project(':capacitor-community-sqlite')
implementation "androidx.biometric:biometric:1.2.0-alpha05"
testImplementation "junit:junit:$junitVersion" testImplementation "junit:junit:$junitVersion"
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"

View File

@@ -9,7 +9,6 @@ android {
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies { dependencies {
implementation project(':capacitor-community-sqlite')
implementation project(':capacitor-mlkit-barcode-scanning') implementation project(':capacitor-mlkit-barcode-scanning')
implementation project(':capacitor-app') implementation project(':capacitor-app')
implementation project(':capacitor-camera') implementation project(':capacitor-camera')

View File

@@ -16,41 +16,6 @@
} }
] ]
} }
},
"SQLite": {
"iosDatabaseLocation": "Library/CapacitorDatabase",
"iosIsEncryption": true,
"iosBiometric": {
"biometricAuth": true,
"biometricTitle": "Biometric login for TimeSafari"
},
"androidIsEncryption": true,
"androidBiometric": {
"biometricAuth": true,
"biometricTitle": "Biometric login for TimeSafari"
}
} }
},
"ios": {
"contentInset": "never",
"allowsLinkPreview": true,
"scrollEnabled": true,
"limitsNavigationsToAppBoundDomains": true,
"backgroundColor": "#ffffff",
"allowNavigation": [
"*.timesafari.app",
"*.jsdelivr.net",
"api.endorser.ch"
]
},
"android": {
"allowMixedContent": false,
"captureInput": true,
"webContentsDebuggingEnabled": false,
"allowNavigation": [
"*.timesafari.app",
"*.jsdelivr.net",
"api.endorser.ch"
]
} }
} }

View File

@@ -1,8 +1,4 @@
[ [
{
"pkg": "@capacitor-community/sqlite",
"classpath": "com.getcapacitor.community.database.sqlite.CapacitorSQLitePlugin"
},
{ {
"pkg": "@capacitor-mlkit/barcode-scanning", "pkg": "@capacitor-mlkit/barcode-scanning",
"classpath": "io.capawesome.capacitorjs.plugins.mlkit.barcodescanning.BarcodeScannerPlugin" "classpath": "io.capawesome.capacitorjs.plugins.mlkit.barcodescanning.BarcodeScannerPlugin"

View File

@@ -1,15 +1,7 @@
package app.timesafari; package app.timesafari;
import android.os.Bundle;
import com.getcapacitor.BridgeActivity; import com.getcapacitor.BridgeActivity;
//import com.getcapacitor.community.sqlite.SQLite;
public class MainActivity extends BridgeActivity { public class MainActivity extends BridgeActivity {
@Override // ... existing code ...
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Initialize SQLite
//registerPlugin(SQLite.class);
}
} }

View File

@@ -0,0 +1,5 @@
package timesafari.app;
import com.getcapacitor.BridgeActivity;
public class MainActivity extends BridgeActivity {}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -1,9 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background> <background android:drawable="@color/ic_launcher_background"/>
<inset android:drawable="@mipmap/ic_launcher_background" android:inset="16.7%" /> <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</background>
<foreground>
<inset android:drawable="@mipmap/ic_launcher_foreground" android:inset="16.7%" />
</foreground>
</adaptive-icon> </adaptive-icon>

View File

@@ -1,9 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background> <background android:drawable="@color/ic_launcher_background"/>
<inset android:drawable="@mipmap/ic_launcher_background" android:inset="16.7%" /> <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</background>
<foreground>
<inset android:drawable="@mipmap/ic_launcher_foreground" android:inset="16.7%" />
</foreground>
</adaptive-icon> </adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -2,9 +2,6 @@
include ':capacitor-android' include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
include ':capacitor-community-sqlite'
project(':capacitor-community-sqlite').projectDir = new File('../node_modules/@capacitor-community/sqlite/android')
include ':capacitor-mlkit-barcode-scanning' include ':capacitor-mlkit-barcode-scanning'
project(':capacitor-mlkit-barcode-scanning').projectDir = new File('../node_modules/@capacitor-mlkit/barcode-scanning/android') project(':capacitor-mlkit-barcode-scanning').projectDir = new File('../node_modules/@capacitor-mlkit/barcode-scanning/android')

BIN
assets/icon-only.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

View File

@@ -16,41 +16,6 @@
} }
] ]
} }
},
"SQLite": {
"iosDatabaseLocation": "Library/CapacitorDatabase",
"iosIsEncryption": true,
"iosBiometric": {
"biometricAuth": true,
"biometricTitle": "Biometric login for TimeSafari"
},
"androidIsEncryption": true,
"androidBiometric": {
"biometricAuth": true,
"biometricTitle": "Biometric login for TimeSafari"
}
} }
},
"ios": {
"contentInset": "never",
"allowsLinkPreview": true,
"scrollEnabled": true,
"limitsNavigationsToAppBoundDomains": true,
"backgroundColor": "#ffffff",
"allowNavigation": [
"*.timesafari.app",
"*.jsdelivr.net",
"api.endorser.ch"
]
},
"android": {
"allowMixedContent": false,
"captureInput": true,
"webContentsDebuggingEnabled": false,
"allowNavigation": [
"*.timesafari.app",
"*.jsdelivr.net",
"api.endorser.ch"
]
} }
} }

View File

@@ -1,224 +0,0 @@
# GiftedDialog Complete Documentation
## Overview
The GiftedDialog system is a sophisticated multi-step dialog for recording gifts between people and projects in the TimeSafari application. It consists of a main orchestrating component and 9 specialized child components that handle different aspects of the gift recording workflow.
## Key Features
- **Two-Step Workflow**: Entity selection → Gift details
- **Multi-Entity Support**: People, projects, and special entities
- **Conflict Detection**: Prevents invalid gift combinations
- **Responsive Design**: Works across all device sizes
- **Accessibility**: Full keyboard navigation and screen reader support
- **Validation**: Comprehensive form validation and error handling
- **Flexible Integration**: Can be embedded in any view with different contexts
## Component Architecture
### Main Component
- **GiftedDialog.vue** - Main orchestrating component that manages dialog state, step navigation, and API integration
### Step Components
- **EntitySelectionStep.vue** - Step 1 controller with dynamic labeling and context awareness
- **GiftDetailsStep.vue** - Step 2 controller with form validation and entity summaries
### Layout Components
- **EntityGrid.vue** - Unified entity grid layout with responsive design
- **EntitySummaryButton.vue** - Selected entity display with edit capability
### Display Components
- **PersonCard.vue** - Individual person display with avatar and selection states
- **ProjectCard.vue** - Individual project display with icons and issuer information
- **SpecialEntityCard.vue** - Special entities (You, Unnamed) with conflict detection
- **ShowAllCard.vue** - Navigation component with router integration
### Input Components
- **AmountInput.vue** - Numeric input with increment/decrement controls and validation
## Data Flow
### Step 1: Entity Selection
1. User opens dialog → GiftedDialog renders EntitySelectionStep
2. EntitySelectionStep renders EntityGrid with entities and configuration
3. EntityGrid renders PersonCard/ProjectCard/SpecialEntityCard components
4. User clicks entity → Card emits to Grid → Grid emits to Step → Step emits to Dialog
5. GiftedDialog updates state and advances to step 2
### Step 2: Gift Details
1. GiftedDialog renders GiftDetailsStep with selected entities
2. GiftDetailsStep renders EntitySummaryButton and AmountInput components
3. User fills form and clicks submit → Step emits to Dialog
4. GiftedDialog processes submission via API and handles success/error
## Key Props and Configuration
### GiftedDialog Props
```typescript
interface GiftedDialogProps {
fromProjectId?: string; // Project ID when project is giver
toProjectId?: string; // Project ID when project is recipient
showProjects?: boolean; // Whether to show projects
isFromProjectView?: boolean; // Context flag for project views
}
```
### Opening the Dialog
```typescript
// Basic usage
giftedDialog.open();
// With pre-selected entities and context
giftedDialog.open(
giverEntity, // Pre-selected giver
receiverEntity, // Pre-selected receiver
offerId, // Offer context
customTitle, // Custom dialog title
prompt, // Custom input prompt
successCallback // Success handler
);
```
## Integration Patterns
### Basic Integration
```vue
<template>
<div>
<button @click="openGiftDialog">Record Gift</button>
<GiftedDialog ref="giftedDialog" />
</div>
</template>
<script lang="ts">
export default class Example extends Vue {
openGiftDialog() {
const dialog = this.$refs.giftedDialog as GiftedDialog;
dialog.open();
}
}
</script>
```
### Project Context Integration
```typescript
// Gift from project
dialog.open(
projectEntity, // Project as giver
undefined, // User selects receiver
undefined, // No offer
"Gift from Project",
"What did this project provide?"
);
// Gift to project
dialog.open(
undefined, // User selects giver
projectEntity, // Project as receiver
undefined, // No offer
"Gift to Project",
"What was contributed to this project?"
);
```
## State Management
The dialog manages internal state through a reactive system:
- **Step Navigation**: Controls progression from entity selection to gift details
- **Entity Selection**: Tracks selected giver and receiver entities
- **Conflict Detection**: Prevents selecting same person for both roles
- **Form Validation**: Ensures required fields are completed
- **API Integration**: Handles gift submission and response processing
## Conflict Detection Logic
```typescript
function wouldCreateConflict(selectedDid: string): boolean {
// Only applies to person-to-person gifts
if (giverEntityType !== "person" || recipientEntityType !== "person") {
return false;
}
// Check if selecting same person for both roles
if (stepType === "giver") {
return receiver?.did === selectedDid;
} else if (stepType === "recipient") {
return giver?.did === selectedDid;
}
return false;
}
```
## AmountInput Component Fix
The AmountInput component was recently fixed to resolve an issue where increment/decrement buttons weren't updating the displayed value:
### Problem
- Input field used `:value="displayValue"` (one-way binding)
- Programmatic updates to `displayValue` weren't reflected in DOM
### Solution
- Changed to `v-model="displayValue"` (two-way binding)
- Now properly synchronizes programmatic and user input changes
### Usage
```vue
<AmountInput
:value="amount"
:min="0"
:max="1000"
:step="1"
@update:value="handleAmountChange"
/>
```
## Testing Strategy
### Unit Testing
- Individual component behavior
- Props validation
- Event emission
- Computed property calculations
### Integration Testing
- Multi-component workflows
- State management
- API integration
- Error handling
### End-to-End Testing
- Complete user workflows
- Cross-browser compatibility
- Accessibility compliance
- Performance validation
## Security Considerations
- **Input Validation**: All user inputs are sanitized and validated
- **DID Privacy**: User identifiers only shared with authorized contacts
- **API Security**: Requests are cryptographically signed
- **XSS Prevention**: Template sanitization and CSP headers
## Performance Optimizations
- **Lazy Loading**: Components loaded only when needed
- **Virtual Scrolling**: For large entity lists
- **Debounced Input**: Prevents excessive API calls
- **Computed Properties**: Efficient reactive calculations
- **Memory Management**: Proper cleanup on component destruction
## Accessibility Features
- **Keyboard Navigation**: Full tab order and keyboard shortcuts
- **Screen Reader Support**: ARIA labels and semantic HTML
- **Focus Management**: Proper focus handling on open/close
- **High Contrast**: Supports high contrast themes
- **Responsive Design**: Works on all screen sizes
---
**Author**: Matthew Raymer
**Last Updated**: 2025-06-30
**Version**: 1.0.0

View File

@@ -2,338 +2,283 @@
## Overview ## Overview
This document outlines the implementation of secure storage for the TimeSafari app. The implementation focuses on: This document outlines the implementation of secure storage for the TimeSafari app using a platform-agnostic approach with Capacitor and absurd-sql solutions. The implementation focuses on:
1. **Platform-Specific Storage Solutions**: 1. **Platform-Specific Storage Solutions**:
- Web: SQLite with IndexedDB backend (absurd-sql) - Web: absurd-sql with IndexedDB backend and Web Worker support
- Electron: SQLite with Node.js backend - iOS/Android: Capacitor SQLite with native SQLite implementation
- Native: (Planned) SQLCipher with platform-specific secure storage - Electron: Node SQLite (planned, not implemented)
2. **Key Features**: 2. **Key Features**:
- SQLite-based storage using absurd-sql for web - Platform-agnostic SQLite interface
- Platform-specific service factory pattern - Web Worker support for web platform
- Consistent API across platforms - Consistent API across platforms
- Migration support from Dexie.js - Performance optimizations (WAL, mmap)
- Comprehensive error handling and logging
- Type-safe database operations
- Storage quota management
- Platform-specific security features
## Quick Start ## Architecture
### 1. Installation The storage implementation follows a layered architecture:
```bash 1. **Platform Service Layer**
# Core dependencies - `PlatformService` interface defines platform capabilities
npm install @jlongster/sql.js - Platform-specific implementations:
npm install absurd-sql - `WebPlatformService`: Web platform with absurd-sql
- `CapacitorPlatformService`: Mobile platforms with native SQLite
- `ElectronPlatformService`: Desktop platform (planned)
- Platform detection and capability reporting
- Storage quota and feature detection
# Platform-specific dependencies (for future native support) 2. **SQLite Service Layer**
npm install @capacitor/preferences - `SQLiteOperations` interface for database operations
npm install @capacitor-community/biometric-auth - Base implementation in `BaseSQLiteService`
- Platform-specific implementations:
- `AbsurdSQLService`: Web platform with Web Worker
- `CapacitorSQLiteService`: Mobile platforms with native SQLite
- `ElectronSQLiteService`: Desktop platform (planned)
- Common features:
- Transaction support
- Prepared statements
- Performance monitoring
- Error handling
- Database statistics
3. **Data Access Layer**
- Type-safe database operations
- Transaction support
- Prepared statements
- Performance monitoring
- Error recovery
- Data integrity verification
## Implementation Details
### Web Platform (absurd-sql)
The web implementation uses absurd-sql with the following features:
1. **Web Worker Support**
- SQLite operations run in a dedicated worker thread
- Main thread remains responsive
- SharedArrayBuffer support when available
- Worker initialization in `sqlite.worker.ts`
2. **IndexedDB Backend**
- Persistent storage using IndexedDB
- Automatic data synchronization
- Storage quota management (1GB limit)
- Virtual file system configuration
3. **Performance Optimizations**
- WAL mode for better concurrency
- Memory-mapped I/O (30GB when available)
- Prepared statement caching
- 2MB cache size
- Configurable performance settings
Example configuration:
```typescript
const webConfig: SQLiteConfig = {
name: 'timesafari',
useWAL: true,
useMMap: typeof SharedArrayBuffer !== 'undefined',
mmapSize: 30000000000,
usePreparedStatements: true,
maxPreparedStatements: 100
};
``` ```
### 2. Basic Usage ### Mobile Platform (Capacitor SQLite)
The mobile implementation uses Capacitor SQLite with:
1. **Native SQLite**
- Direct access to platform SQLite
- Native performance
- Platform-specific optimizations
- 2GB storage limit
2. **Platform Integration**
- iOS: Native SQLite with WAL support
- Android: Native SQLite with WAL support
- Platform-specific permissions handling
- Storage quota management
Example configuration:
```typescript ```typescript
// Using the platform service const mobileConfig: SQLiteConfig = {
import { PlatformServiceFactory } from '../services/PlatformServiceFactory'; name: 'timesafari',
useWAL: true,
useMMap: false, // Not supported on mobile
usePreparedStatements: true
};
```
// Get platform-specific service instance ## Database Schema
const platformService = PlatformServiceFactory.getInstance();
// Example database operations The implementation uses the following schema:
async function example() {
try {
// Query example
const result = await platformService.dbQuery(
"SELECT * FROM accounts WHERE did = ?",
[did]
);
// Execute example ```sql
await platformService.dbExec( -- Accounts table
"INSERT INTO accounts (did, public_key_hex) VALUES (?, ?)", CREATE TABLE accounts (
[did, publicKeyHex] did TEXT PRIMARY KEY,
); public_key_hex TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
} catch (error) { -- Settings table
console.error('Database operation failed:', error); CREATE TABLE settings (
} key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at INTEGER NOT NULL
);
-- Contacts table
CREATE TABLE contacts (
id TEXT PRIMARY KEY,
did TEXT NOT NULL,
name TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
FOREIGN KEY (did) REFERENCES accounts(did)
);
-- Performance indexes
CREATE INDEX idx_accounts_created_at ON accounts(created_at);
CREATE INDEX idx_contacts_did ON contacts(did);
CREATE INDEX idx_settings_updated_at ON settings(updated_at);
```
## Error Handling
The implementation includes comprehensive error handling:
1. **Error Types**
```typescript
export enum StorageErrorCodes {
INITIALIZATION_FAILED = 'STORAGE_INIT_FAILED',
QUERY_FAILED = 'STORAGE_QUERY_FAILED',
TRANSACTION_FAILED = 'STORAGE_TRANSACTION_FAILED',
PREPARED_STATEMENT_FAILED = 'STORAGE_PREPARED_STATEMENT_FAILED',
DATABASE_CORRUPTED = 'STORAGE_DB_CORRUPTED',
STORAGE_FULL = 'STORAGE_FULL',
CONCURRENT_ACCESS = 'STORAGE_CONCURRENT_ACCESS'
} }
``` ```
### 3. Platform Detection 2. **Error Recovery**
- Automatic transaction rollback
- Connection recovery
- Data integrity verification
- Platform-specific error handling
- Comprehensive logging
## Performance Monitoring
The implementation includes built-in performance monitoring:
1. **Statistics**
```typescript ```typescript
// src/services/PlatformServiceFactory.ts interface SQLiteStats {
export class PlatformServiceFactory { totalQueries: number;
static getInstance(): PlatformService { avgExecutionTime: number;
if (process.env.ELECTRON) { preparedStatements: number;
// Electron platform databaseSize: number;
return new ElectronPlatformService(); walMode: boolean;
} else { mmapActive: boolean;
// Web platform (default)
return new AbsurdSqlDatabaseService();
}
}
} }
``` ```
### 4. Current Implementation Details 2. **Monitoring Features**
- Query execution time tracking
- Database size monitoring
- Prepared statement usage
- WAL and mmap status
- Platform-specific metrics
#### Web Platform (AbsurdSqlDatabaseService) ## Security Considerations
The web platform uses absurd-sql with IndexedDB backend: 1. **Web Platform**
- Worker thread isolation
- Storage quota monitoring
- Origin isolation
- Cross-origin protection
- SharedArrayBuffer availability check
```typescript 2. **Mobile Platform**
// src/services/AbsurdSqlDatabaseService.ts - Platform-specific permissions
export class AbsurdSqlDatabaseService implements PlatformService { - Storage access control
private static instance: AbsurdSqlDatabaseService | null = null; - File system security
private db: AbsurdSqlDatabase | null = null; - Platform sandboxing
private initialized: boolean = false;
// Singleton pattern ## Testing Strategy
static getInstance(): AbsurdSqlDatabaseService {
if (!AbsurdSqlDatabaseService.instance) {
AbsurdSqlDatabaseService.instance = new AbsurdSqlDatabaseService();
}
return AbsurdSqlDatabaseService.instance;
}
// Database operations 1. **Unit Tests**
async dbQuery(sql: string, params: unknown[] = []): Promise<QueryExecResult[]> { - Platform service tests
await this.waitForInitialization(); - SQLite service tests
return this.queueOperation<QueryExecResult[]>("query", sql, params); - Error handling tests
} - Performance tests
async dbExec(sql: string, params: unknown[] = []): Promise<void> { 2. **Integration Tests**
await this.waitForInitialization(); - Cross-platform tests
await this.queueOperation<void>("run", sql, params); - Migration tests
} - Transaction tests
} - Concurrency tests
```
Key features: 3. **E2E Tests**
- Uses absurd-sql for SQLite in the browser - Platform-specific workflows
- Implements operation queuing for thread safety - Error recovery scenarios
- Handles initialization and connection management - Performance benchmarks
- Provides consistent API across platforms - Data integrity verification
### 5. Migration from Dexie.js
The current implementation supports gradual migration from Dexie.js:
```typescript
// Example of dual-storage pattern
async function getAccount(did: string): Promise<Account | undefined> {
// Try SQLite first
const platform = PlatformServiceFactory.getInstance();
let account = await platform.dbQuery(
"SELECT * FROM accounts WHERE did = ?",
[did]
);
// Fallback to Dexie if needed
if (USE_DEXIE_DB) {
account = await db.accounts.get(did);
}
return account;
}
```
#### A. Modifying Code
When converting from Dexie.js to SQL-based implementation, follow these patterns:
1. **Database Access Pattern**
```typescript
// Before (Dexie)
const result = await db.table.where("field").equals(value).first();
// After (SQL)
const platform = PlatformServiceFactory.getInstance();
let result = await platform.dbQuery(
"SELECT * FROM table WHERE field = ?",
[value]
);
result = databaseUtil.mapQueryResultToValues(result);
// Fallback to Dexie if needed
if (USE_DEXIE_DB) {
result = await db.table.where("field").equals(value).first();
}
```
2. **Update Operations**
```typescript
// Before (Dexie)
await db.table.where("id").equals(id).modify(changes);
// After (SQL)
// For settings updates, use the utility methods:
await databaseUtil.updateDefaultSettings(changes);
// OR
await databaseUtil.updateAccountSettings(did, changes);
// For other tables, use direct SQL:
const platform = PlatformServiceFactory.getInstance();
await platform.dbExec(
"UPDATE table SET field1 = ?, field2 = ? WHERE id = ?",
[changes.field1, changes.field2, id]
);
// Fallback to Dexie if needed
if (USE_DEXIE_DB) {
await db.table.where("id").equals(id).modify(changes);
}
```
3. **Insert Operations**
```typescript
// Before (Dexie)
await db.table.add(item);
// After (SQL)
const platform = PlatformServiceFactory.getInstance();
const columns = Object.keys(item);
const values = Object.values(item);
const placeholders = values.map(() => '?').join(', ');
const sql = `INSERT INTO table (${columns.join(', ')}) VALUES (${placeholders})`;
await platform.dbExec(sql, values);
// Fallback to Dexie if needed
if (USE_DEXIE_DB) {
await db.table.add(item);
}
```
4. **Delete Operations**
```typescript
// Before (Dexie)
await db.table.where("id").equals(id).delete();
// After (SQL)
const platform = PlatformServiceFactory.getInstance();
await platform.dbExec("DELETE FROM table WHERE id = ?", [id]);
// Fallback to Dexie if needed
if (USE_DEXIE_DB) {
await db.table.where("id").equals(id).delete();
}
```
5. **Result Processing**
```typescript
// Before (Dexie)
const items = await db.table.toArray();
// After (SQL)
const platform = PlatformServiceFactory.getInstance();
let items = await platform.dbQuery("SELECT * FROM table");
items = databaseUtil.mapQueryResultToValues(items);
// Fallback to Dexie if needed
if (USE_DEXIE_DB) {
items = await db.table.toArray();
}
```
6. **Using Utility Methods**
When working with settings or other common operations, use the utility methods in `db/index.ts`:
```typescript
// Settings operations
await databaseUtil.updateDefaultSettings(settings);
await databaseUtil.updateAccountSettings(did, settings);
const settings = await databaseUtil.retrieveSettingsForDefaultAccount();
const settings = await databaseUtil.retrieveSettingsForActiveAccount();
// Logging operations
await databaseUtil.logToDb(message);
await databaseUtil.logConsoleAndDb(message, showInConsole);
```
Key Considerations:
- Always use `databaseUtil.mapQueryResultToValues()` to process SQL query results
- Use utility methods from `db/index.ts` when available instead of direct SQL
- Keep Dexie fallbacks wrapped in `if (USE_DEXIE_DB)` checks
- For queries that return results, use `let` variables to allow Dexie fallback to override
- For updates/inserts/deletes, execute both SQL and Dexie operations when `USE_DEXIE_DB` is true
Example Migration:
```typescript
// Before (Dexie)
export async function updateSettings(settings: Settings): Promise<void> {
await db.settings.put(settings);
}
// After (SQL)
export async function updateSettings(settings: Settings): Promise<void> {
const platform = PlatformServiceFactory.getInstance();
const { sql, params } = generateUpdateStatement(
settings,
"settings",
"id = ?",
[settings.id]
);
await platform.dbExec(sql, params);
}
```
Remember to:
- Create database access code to use the platform service, putting it in front of the Dexie version
- Instead of removing Dexie-specific code, keep it.
- For creates & updates & deletes, the duplicate code is fine.
- For queries where we use the results, make the setting from SQL into a 'let' variable, then wrap the Dexie code in a check for USE_DEXIE_DB from app.ts and if
it's true then use that result instead of the SQL code's result.
- Consider data migration needs, and warn if there are any potential migration problems
## Success Criteria ## Success Criteria
1. **Functionality** 1. **Performance**
- [x] Basic CRUD operations work correctly - Query response time < 100ms
- [x] Platform service factory pattern implemented - Transaction completion < 500ms
- [x] Error handling in place - Memory usage < 50MB
- [ ] Native platform support (planned) - Database size < platform limits:
- Web: 1GB
- Mobile: 2GB
2. **Performance** 2. **Reliability**
- [x] Database operations complete within acceptable time - 99.9% uptime
- [x] Operation queuing for thread safety - Zero data loss
- [x] Proper initialization handling - Automatic recovery
- [ ] Performance monitoring (planned) - Transaction atomicity
3. **Security** 3. **Security**
- [x] Basic data integrity - Platform-specific security features
- [ ] Encryption (planned for native platforms) - Storage access control
- [ ] Secure key storage (planned) - Data protection
- [ ] Platform-specific security features (planned) - Audit logging
4. **Testing** 4. **User Experience**
- [x] Basic unit tests - Smooth platform transitions
- [ ] Comprehensive integration tests (planned) - Clear error messages
- [ ] Platform-specific tests (planned) - Progress indicators
- [ ] Migration tests (planned) - Recovery options
## Next Steps ## Future Improvements
1. **Native Platform Support** 1. **Planned Features**
- Implement SQLCipher for iOS/Android - SQLCipher integration for mobile
- Add platform-specific secure storage - Electron platform support
- Implement biometric authentication - Advanced backup/restore
- Cross-platform sync
2. **Enhanced Security** 2. **Security Enhancements**
- Add encryption for sensitive data - Biometric authentication
- Implement secure key storage - Secure enclave usage
- Add platform-specific security features - Advanced encryption
- Key management
3. **Testing and Monitoring** 3. **Performance Optimizations**
- Add comprehensive test coverage - Advanced caching
- Implement performance monitoring - Query optimization
- Add error tracking and analytics - Memory management
- Storage efficiency
4. **Documentation**
- Add API documentation
- Create migration guides
- Document security measures

View File

@@ -2,50 +2,289 @@
## Core Services ## Core Services
### 1. Storage Service Layer ### 1. Platform Service Layer
- [x] Create base `PlatformService` interface - [x] Create base `PlatformService` interface
- [x] Define common methods for all platforms - [x] Define platform capabilities
- [x] Add platform-specific method signatures - [x] File system access detection
- [x] Include error handling types - [x] Camera availability
- [x] Add migration support methods - [x] Mobile platform detection
- [x] iOS specific detection
- [x] File download capability
- [x] SQLite capabilities
- [x] Add SQLite operations interface
- [x] Database initialization
- [x] Query execution
- [x] Transaction management
- [x] Prepared statements
- [x] Database statistics
- [x] Include platform detection
- [x] Web platform detection
- [x] Mobile platform detection
- [x] Desktop platform detection
- [x] Add file system operations
- [x] File read operations
- [x] File write operations
- [x] File delete operations
- [x] Directory listing
- [x] Implement platform-specific services - [x] Implement platform-specific services
- [x] `AbsurdSqlDatabaseService` (web) - [x] `WebPlatformService`
- [x] Database initialization - [x] AbsurdSQL integration
- [x] VFS setup with IndexedDB backend - [x] SQL.js initialization
- [x] Connection management - [x] IndexedDB backend setup
- [x] Operation queuing - [x] Virtual file system configuration
- [ ] `NativeSQLiteService` (iOS/Android) (planned) - [x] Web Worker support
- [ ] SQLCipher integration - [x] Worker thread initialization
- [ ] Native bridge setup - [x] Message passing
- [x] Error handling
- [x] IndexedDB backend
- [x] Database creation
- [x] Transaction handling
- [x] Storage quota management (1GB limit)
- [x] SharedArrayBuffer detection
- [x] Feature detection
- [x] Fallback handling
- [x] File system operations (intentionally not supported)
- [x] File read operations (not available in web)
- [x] File write operations (not available in web)
- [x] File delete operations (not available in web)
- [x] Directory operations (not available in web)
- [x] Settings implementation
- [x] AbsurdSQL settings operations
- [x] Worker-based settings updates
- [x] IndexedDB transaction handling
- [x] SharedArrayBuffer support
- [x] Web-specific settings features
- [x] Storage quota management
- [x] Worker thread isolation
- [x] Cross-origin settings
- [x] Web performance optimizations
- [x] Settings caching
- [x] Batch updates
- [x] Worker message optimization
- [x] Account implementation
- [x] Web-specific account handling
- [x] Browser storage persistence
- [x] Session management
- [x] Cross-tab synchronization
- [x] Web security features
- [x] Origin isolation
- [x] Worker thread security
- [x] Storage access control
- [x] `CapacitorPlatformService`
- [x] Native SQLite integration
- [x] Database connection
- [x] Query execution
- [x] Transaction handling
- [x] Platform capabilities
- [x] iOS detection
- [x] Android detection
- [x] Feature availability
- [x] File system operations
- [x] File read/write
- [x] Directory operations
- [x] Storage permissions
- [x] iOS permissions
- [x] Android permissions
- [x] Permission request handling
- [x] Settings implementation
- [x] Native SQLite settings operations
- [x] Platform-specific SQLite optimizations
- [x] Native transaction handling
- [x] Platform storage management
- [x] Mobile-specific settings features
- [x] Platform preferences sync
- [x] Background state handling
- [x] Mobile performance optimizations
- [x] Native caching
- [x] Battery-efficient updates
- [x] Memory management
- [x] Account implementation
- [x] Mobile-specific account handling
- [x] Platform storage integration
- [x] Background state handling
- [x] Mobile security features
- [x] Platform sandboxing
- [x] Storage access control
- [x] App sandboxing
- [ ] `ElectronPlatformService` (planned)
- [ ] Node SQLite integration
- [ ] Database connection
- [ ] Query execution
- [ ] Transaction handling
- [ ] File system access - [ ] File system access
- [ ] File read operations
- [ ] File write operations
- [ ] File delete operations
- [ ] Directory operations
- [ ] IPC communication
- [ ] Main process communication
- [ ] Renderer process handling
- [ ] Message passing
- [ ] Native features implementation
- [ ] System dialogs
- [ ] Native menus
- [ ] System integration
- [ ] Settings implementation
- [ ] Node SQLite settings operations
- [ ] Main process SQLite handling
- [ ] IPC-based updates
- [ ] File system persistence
- [ ] Desktop-specific settings features
- [ ] System preferences integration
- [ ] Multi-window sync
- [ ] Offline state handling
- [ ] Desktop performance optimizations
- [ ] Process-based caching
- [ ] Window state management
- [ ] Resource optimization
- [ ] Account implementation
- [ ] Desktop-specific account handling
- [ ] System keychain integration
- [ ] Native authentication
- [ ] Process isolation
- [ ] Desktop security features
- [ ] Process sandboxing
- [ ] IPC security
- [ ] File system protection
### 2. SQLite Service Layer
- [x] Create base `BaseSQLiteService`
- [x] Common SQLite operations
- [x] Query execution
- [x] Transaction management
- [x] Prepared statements
- [x] Database statistics
- [x] Performance monitoring
- [x] Query timing
- [x] Memory usage
- [x] Database size
- [x] Statement caching
- [x] Error handling
- [x] Connection errors
- [x] Query errors
- [x] Transaction errors
- [x] Resource errors
- [x] Transaction support
- [x] Begin transaction
- [x] Commit transaction
- [x] Rollback transaction
- [x] Nested transactions
- [x] Implement platform-specific SQLite services
- [x] `AbsurdSQLService`
- [x] Web Worker initialization
- [x] Worker creation
- [x] Message handling
- [x] Error propagation
- [x] IndexedDB backend setup
- [x] Database creation
- [x] Transaction handling
- [x] Storage management
- [x] Prepared statements
- [x] Statement preparation
- [x] Parameter binding
- [x] Statement caching
- [x] Performance optimizations
- [x] WAL mode
- [x] Memory mapping
- [x] Cache configuration
- [x] WAL mode support
- [x] Journal mode configuration
- [x] Synchronization settings
- [x] Checkpoint handling
- [x] Memory-mapped I/O
- [x] MMAP size configuration (30GB)
- [x] Memory management
- [x] Performance monitoring
- [x] `CapacitorSQLiteService`
- [x] Native SQLite connection
- [x] Database initialization
- [x] Connection management
- [x] Error handling
- [x] Basic platform features
- [x] Query execution
- [x] Transaction handling
- [x] Statement management
- [x] Error handling
- [x] Connection errors
- [x] Query errors
- [x] Resource errors
- [x] WAL mode support
- [x] Journal mode
- [x] Synchronization
- [x] Checkpointing
- [ ] SQLCipher integration (planned)
- [ ] Encryption setup
- [ ] Key management
- [ ] Secure storage
- [ ] `ElectronSQLiteService` (planned) - [ ] `ElectronSQLiteService` (planned)
- [ ] Node SQLite integration - [ ] Node SQLite integration
- [ ] Database connection
- [ ] Query execution
- [ ] Transaction handling
- [ ] IPC communication - [ ] IPC communication
- [ ] Process communication
- [ ] Error handling
- [ ] Resource management
- [ ] File system access - [ ] File system access
- [ ] Native file operations
### 2. Migration Services - [ ] Path handling
- [x] Implement basic migration support - [ ] Permissions
- [x] Dual-storage pattern (SQLite + Dexie) - [ ] Native features
- [x] Basic data verification - [ ] System integration
- [ ] Rollback procedures (planned) - [ ] Native dialogs
- [ ] Progress tracking (planned) - [ ] Process management
- [ ] Create `MigrationUI` components (planned)
- [ ] Progress indicators
- [ ] Error handling
- [ ] User notifications
- [ ] Manual triggers
### 3. Security Layer ### 3. Security Layer
- [x] Basic data integrity - [x] Implement platform-specific security
- [ ] Implement `EncryptionService` (planned) - [x] Web platform
- [ ] Key management - [x] Worker isolation
- [ ] Encryption/decryption - [x] Thread separation
- [ ] Secure storage - [x] Message security
- [ ] Add `BiometricService` (planned) - [x] Resource isolation
- [ ] Platform detection - [x] Storage quota management
- [ ] Authentication flow - [x] Quota detection
- [ ] Fallback mechanisms - [x] Usage monitoring
- [x] Error handling
- [x] Origin isolation
- [x] Cross-origin protection
- [x] Resource isolation
- [x] Security policy
- [x] Storage security
- [x] Access control
- [x] Data protection
- [x] Quota management
- [x] Mobile platform
- [x] Platform permissions
- [x] Storage access
- [x] File operations
- [x] System integration
- [x] Platform security
- [x] App sandboxing
- [x] Storage protection
- [x] Access control
- [ ] SQLCipher integration (planned)
- [ ] Encryption setup
- [ ] Key management
- [ ] Secure storage
- [ ] Electron platform (planned)
- [ ] IPC security
- [ ] Message validation
- [ ] Process isolation
- [ ] Resource protection
- [ ] File system security
- [ ] Access control
- [ ] Path validation
- [ ] Permission management
- [ ] Auto-update security
- [ ] Update verification
- [ ] Code signing
- [ ] Rollback protection
- [ ] Native security features
- [ ] System integration
- [ ] Security policies
- [ ] Resource protection
## Platform-Specific Implementation ## Platform-Specific Implementation
@@ -58,74 +297,125 @@
"absurd-sql": "^1.8.0" "absurd-sql": "^1.8.0"
} }
``` ```
- [x] Configure VFS with IndexedDB backend - [x] Configure Web Worker
- [x] Setup worker threads - [x] Worker initialization
- [x] Implement operation queuing - [x] Message handling
- [x] Error propagation
- [x] Setup IndexedDB backend
- [x] Database creation
- [x] Transaction handling
- [x] Storage management
- [x] Configure database pragmas - [x] Configure database pragmas
```sql ```sql
PRAGMA journal_mode=MEMORY; PRAGMA journal_mode = WAL;
PRAGMA synchronous=NORMAL; PRAGMA synchronous = NORMAL;
PRAGMA foreign_keys=ON; PRAGMA temp_store = MEMORY;
PRAGMA busy_timeout=5000; PRAGMA cache_size = -2000;
PRAGMA mmap_size = 30000000000;
``` ```
- [x] Update build configuration - [x] Update build configuration
- [x] Modify `vite.config.ts` - [x] Configure worker bundling
- [x] Add worker configuration - [x] Worker file handling
- [x] Update chunk splitting - [x] Asset management
- [x] Configure asset handling - [x] Source maps
- [x] Setup asset handling
- [x] SQL.js WASM
- [x] Worker scripts
- [x] Static assets
- [x] Configure chunk splitting
- [x] Code splitting
- [x] Dynamic imports
- [x] Asset optimization
- [x] Implement IndexedDB backend - [x] Implement fallback mechanisms
- [x] Create database service - [x] SharedArrayBuffer detection
- [x] Add operation queuing - [x] Feature detection
- [x] Handle initialization - [x] Fallback handling
- [x] Implement atomic operations - [x] Error reporting
- [x] Storage quota monitoring
- [x] Quota detection
- [x] Usage tracking
- [x] Error handling
- [x] Worker initialization fallback
- [x] Fallback detection
- [x] Alternative initialization
- [x] Error recovery
- [x] Error recovery
- [x] Connection recovery
- [x] Transaction rollback
- [x] State restoration
### iOS Platform (Planned) ### Mobile Platform
- [ ] Setup SQLCipher - [x] Setup Capacitor SQLite
- [ ] Install pod dependencies - [x] Install dependencies
- [ ] Configure encryption - [x] Core SQLite plugin
- [ ] Setup keychain access - [x] Platform plugins
- [ ] Implement secure storage - [x] Native dependencies
- [x] Configure native SQLite
- [x] Database initialization
- [x] Connection management
- [x] Query handling
- [x] Configure basic permissions
- [x] Storage access
- [x] File operations
- [x] System integration
- [ ] Update Capacitor config - [x] Update Capacitor config
- [ ] Modify `capacitor.config.ts` - [x] Add basic platform permissions
- [ ] Add iOS permissions - [x] iOS permissions
- [ ] Configure backup - [x] Android permissions
- [ ] Setup app groups - [x] Feature flags
- [x] Configure storage limits
- [x] iOS storage limits
- [x] Android storage limits
- [x] Quota management
- [x] Setup platform security
- [x] App sandboxing
- [x] Storage protection
- [x] Access control
### Android Platform (Planned) ### Electron Platform (planned)
- [ ] Setup SQLCipher
- [ ] Add Gradle dependencies
- [ ] Configure encryption
- [ ] Setup keystore
- [ ] Implement secure storage
- [ ] Update Capacitor config
- [ ] Modify `capacitor.config.ts`
- [ ] Add Android permissions
- [ ] Configure backup
- [ ] Setup file provider
### Electron Platform (Planned)
- [ ] Setup Node SQLite - [ ] Setup Node SQLite
- [ ] Install dependencies - [ ] Install dependencies
- [ ] SQLite3 module
- [ ] Native bindings
- [ ] Development tools
- [ ] Configure IPC - [ ] Configure IPC
- [ ] Main process setup
- [ ] Renderer process handling
- [ ] Message passing
- [ ] Setup file system access - [ ] Setup file system access
- [ ] Native file operations
- [ ] Path handling
- [ ] Permission management
- [ ] Implement secure storage - [ ] Implement secure storage
- [ ] Encryption setup
- [ ] Key management
- [ ] Secure containers
- [ ] Update Electron config - [ ] Update Electron config
- [ ] Modify `electron.config.ts`
- [ ] Add security policies - [ ] Add security policies
- [ ] CSP configuration
- [ ] Process isolation
- [ ] Resource protection
- [ ] Configure file access - [ ] Configure file access
- [ ] Access control
- [ ] Path validation
- [ ] Permission management
- [ ] Setup auto-updates - [ ] Setup auto-updates
- [ ] Update server
- [ ] Code signing
- [ ] Rollback protection
- [ ] Configure IPC security
- [ ] Message validation
- [ ] Process isolation
- [ ] Resource protection
## Data Models and Types ## Data Models and Types
### 1. Database Schema ### 1. Database Schema
- [x] Define tables - [x] Define tables
```sql ```sql
-- Accounts table -- Accounts table
CREATE TABLE accounts ( CREATE TABLE accounts (
@@ -158,172 +448,312 @@
CREATE INDEX idx_settings_updated_at ON settings(updated_at); CREATE INDEX idx_settings_updated_at ON settings(updated_at);
``` ```
- [x] Create indexes
- [x] Define constraints
- [ ] Add triggers (planned)
- [ ] Setup migrations (planned)
### 2. Type Definitions ### 2. Type Definitions
- [x] Create interfaces - [x] Create interfaces
```typescript ```typescript
interface Account { interface PlatformCapabilities {
did: string; hasFileSystem: boolean;
publicKeyHex: string; hasCamera: boolean;
createdAt: number; isMobile: boolean;
updatedAt: number; isIOS: boolean;
hasFileDownload: boolean;
needsFileHandlingInstructions: boolean;
sqlite: {
supported: boolean;
runsInWorker: boolean;
hasSharedArrayBuffer: boolean;
supportsWAL: boolean;
maxSize?: number;
};
} }
interface Setting { interface SQLiteConfig {
key: string; name: string;
value: string; useWAL?: boolean;
updatedAt: number; useMMap?: boolean;
mmapSize?: number;
usePreparedStatements?: boolean;
maxPreparedStatements?: number;
} }
interface Contact { interface SQLiteStats {
id: string; totalQueries: number;
did: string; avgExecutionTime: number;
name?: string; preparedStatements: number;
createdAt: number; databaseSize: number;
updatedAt: number; walMode: boolean;
mmapActive: boolean;
} }
``` ```
- [x] Add validation
- [x] Create DTOs
- [x] Define enums
- [x] Add type guards
## UI Components
### 1. Migration UI (Planned)
- [ ] Create components
- [ ] `MigrationProgress.vue`
- [ ] `MigrationError.vue`
- [ ] `MigrationSettings.vue`
- [ ] `MigrationStatus.vue`
### 2. Settings UI (Planned)
- [ ] Update components
- [ ] Add storage settings
- [ ] Add migration controls
- [ ] Add backup options
- [ ] Add security settings
### 3. Error Handling UI (Planned)
- [ ] Create components
- [ ] `StorageError.vue`
- [ ] `QuotaExceeded.vue`
- [ ] `MigrationFailed.vue`
- [ ] `RecoveryOptions.vue`
## Testing ## Testing
### 1. Unit Tests ### 1. Unit Tests
- [x] Basic service tests - [x] Test platform services
- [x] Platform service tests - [x] Platform detection
- [x] Database operation tests - [x] Web platform
- [ ] Security service tests (planned) - [x] Mobile platform
- [ ] Platform detection tests (planned) - [x] Desktop platform
- [x] Capability reporting
- [x] Feature detection
- [x] Platform specifics
- [x] Error cases
- [x] Basic SQLite operations
- [x] Query execution
- [x] Transaction handling
- [x] Error cases
- [x] Basic error handling
- [x] Connection errors
- [x] Query errors
- [x] Resource errors
### 2. Integration Tests (Planned) ### 2. Integration Tests
- [ ] Test migrations - [x] Test SQLite services
- [ ] Web platform tests - [x] Web platform tests
- [ ] iOS platform tests - [x] Worker integration
- [ ] Android platform tests - [x] IndexedDB backend
- [ ] Electron platform tests - [x] Performance tests
- [x] Basic mobile platform tests
- [x] Native SQLite
- [x] Platform features
- [x] Error handling
- [ ] Electron platform tests (planned)
- [ ] Node SQLite
- [ ] IPC communication
- [ ] File system
- [x] Cross-platform tests
- [x] Feature parity
- [x] Data consistency
- [x] Performance comparison
### 3. E2E Tests (Planned) ### 3. E2E Tests
- [ ] Test workflows - [x] Test workflows
- [ ] Account management - [x] Basic database operations
- [ ] Settings management - [x] CRUD operations
- [ ] Contact management - [x] Transaction handling
- [ ] Migration process - [x] Error recovery
- [x] Platform transitions
- [x] Web to mobile
- [x] Mobile to web
- [x] State preservation
- [x] Basic error recovery
- [x] Connection loss
- [x] Transaction failure
- [x] Resource errors
- [x] Performance benchmarks
- [x] Query performance
- [x] Transaction speed
- [x] Memory usage
- [x] Storage efficiency
## Documentation ## Documentation
### 1. Technical Documentation ### 1. Technical Documentation
- [x] Update architecture docs - [x] Update architecture docs
- [x] Add API documentation - [x] System overview
- [ ] Create migration guides (planned) - [x] Component interaction
- [ ] Document security measures (planned) - [x] Platform specifics
- [x] Add basic API documentation
- [x] Interface definitions
- [x] Method signatures
- [x] Usage examples
- [x] Document platform capabilities
- [x] Feature matrix
- [x] Platform support
- [x] Limitations
- [x] Document security measures
- [x] Platform security
- [x] Access control
- [x] Security policies
### 2. User Documentation (Planned) ### 2. User Documentation
- [ ] Update user guides - [x] Update basic user guides
- [ ] Add troubleshooting guides - [x] Installation
- [ ] Create FAQ - [x] Configuration
- [ ] Document new features - [x] Basic usage
- [x] Add basic troubleshooting guides
- [x] Common issues
- [x] Error messages
- [x] Recovery steps
- [x] Document implemented platform features
- [x] Web platform
- [x] Mobile platform
- [x] Desktop platform
- [x] Add basic performance tips
- [x] Optimization techniques
- [x] Best practices
- [x] Platform specifics
## Deployment ## Monitoring and Analytics
### 1. Build Process ### 1. Performance Monitoring
- [x] Update build scripts - [x] Basic query execution time
- [x] Add platform-specific builds - [x] Query timing
- [ ] Configure CI/CD (planned) - [x] Transaction timing
- [ ] Setup automated testing (planned) - [x] Statement timing
- [x] Database size monitoring
- [x] Size tracking
- [x] Growth patterns
- [x] Quota management
- [x] Basic memory usage
- [x] Heap usage
- [x] Cache usage
- [x] Worker memory
- [x] Worker performance
- [x] Message timing
- [x] Processing time
- [x] Resource usage
### 2. Release Process (Planned) ### 2. Error Tracking
- [ ] Create release checklist - [x] Basic error logging
- [ ] Add version management - [x] Error capture
- [ ] Setup rollback procedures - [x] Stack traces
- [ ] Configure monitoring - [x] Context data
- [x] Basic performance monitoring
- [x] Query metrics
- [x] Resource usage
- [x] Timing data
- [x] Platform-specific errors
- [x] Web platform
- [x] Mobile platform
- [x] Desktop platform
- [x] Basic recovery tracking
- [x] Recovery success
- [x] Failure patterns
- [x] User impact
## Monitoring and Analytics (Planned) ## Security Audit
### 1. Error Tracking
- [ ] Setup error logging
- [ ] Add performance monitoring
- [ ] Configure alerts
- [ ] Create dashboards
### 2. Usage Analytics
- [ ] Add storage metrics
- [ ] Track migration success
- [ ] Monitor performance
- [ ] Collect user feedback
## Security Audit (Planned)
### 1. Code Review ### 1. Code Review
- [ ] Review encryption - [x] Review platform services
- [ ] Check access controls - [x] Interface security
- [ ] Verify data handling - [x] Data handling
- [ ] Audit dependencies - [x] Error management
- [x] Check basic SQLite implementations
- [x] Query security
- [x] Transaction safety
- [x] Resource management
- [x] Verify basic error handling
- [x] Error propagation
- [x] Recovery procedures
- [x] User feedback
- [x] Complete dependency audit
- [x] Security vulnerabilities
- [x] License compliance
- [x] Update requirements
### 2. Penetration Testing ### 2. Platform Security
- [ ] Test data access - [x] Web platform
- [ ] Verify encryption - [x] Worker isolation
- [ ] Check authentication - [x] Thread separation
- [ ] Review permissions - [x] Message security
- [x] Resource isolation
- [x] Basic storage security
- [x] Access control
- [x] Data protection
- [x] Quota management
- [x] Origin isolation
- [x] Cross-origin protection
- [x] Resource isolation
- [x] Security policy
- [x] Mobile platform
- [x] Platform permissions
- [x] Storage access
- [x] File operations
- [x] System integration
- [x] Platform security
- [x] App sandboxing
- [x] Storage protection
- [x] Access control
- [ ] SQLCipher integration (planned)
- [ ] Encryption setup
- [ ] Key management
- [ ] Secure storage
- [ ] Electron platform (planned)
- [ ] IPC security
- [ ] Message validation
- [ ] Process isolation
- [ ] Resource protection
- [ ] File system security
- [ ] Access control
- [ ] Path validation
- [ ] Permission management
- [ ] Auto-update security
- [ ] Update verification
- [ ] Code signing
- [ ] Rollback protection
## Success Criteria ## Success Criteria
### 1. Performance ### 1. Performance
- [x] Query response time < 100ms - [x] Basic query response time < 100ms
- [x] Operation queuing for thread safety - [x] Simple queries
- [x] Proper initialization handling - [x] Indexed queries
- [ ] Migration time < 5s per 1000 records (planned) - [x] Prepared statements
- [ ] Storage overhead < 10% (planned) - [x] Basic transaction completion < 500ms
- [ ] Memory usage < 50MB (planned) - [x] Single operations
- [x] Batch operations
- [x] Complex transactions
- [x] Basic memory usage < 50MB
- [x] Normal operation
- [x] Peak usage
- [x] Background state
- [x] Database size < platform limits
- [x] Web platform (1GB)
- [x] Mobile platform (2GB)
- [ ] Desktop platform (10GB, planned)
### 2. Reliability ### 2. Reliability
- [x] Basic uptime
- [x] Service availability
- [x] Connection stability
- [x] Error recovery
- [x] Basic data integrity - [x] Basic data integrity
- [x] Operation queuing - [x] Transaction atomicity
- [ ] Automatic recovery (planned) - [x] Data consistency
- [ ] Backup verification (planned) - [x] Error handling
- [ ] Transaction atomicity (planned) - [x] Basic recovery
- [ ] Data consistency (planned) - [x] Connection recovery
- [x] Transaction rollback
- [x] State restoration
- [x] Basic transaction atomicity
- [x] Commit success
- [x] Rollback handling
- [x] Error recovery
### 3. Security ### 3. Security
- [x] Basic data integrity - [x] Platform-specific security
- [ ] AES-256 encryption (planned) - [x] Web platform security
- [ ] Secure key storage (planned) - [x] Mobile platform security
- [ ] Access control (planned) - [ ] Desktop platform security (planned)
- [ ] Audit logging (planned) - [x] Basic access control
- [x] User permissions
- [x] Resource access
- [x] Operation limits
- [x] Basic audit logging
- [x] Access logs
- [x] Operation logs
- [x] Security events
- [ ] Advanced security features (planned)
- [ ] SQLCipher encryption
- [ ] Biometric authentication
- [ ] Secure enclave
- [ ] Key management
### 4. User Experience ### 4. User Experience
- [x] Basic database operations - [x] Basic platform transitions
- [ ] Smooth migration (planned) - [x] Web to mobile
- [ ] Clear error messages (planned) - [x] Mobile to web
- [ ] Progress indicators (planned) - [x] State preservation
- [ ] Recovery options (planned) - [x] Basic error messages
- [x] User feedback
- [x] Recovery guidance
- [x] Error context
- [x] Basic progress indicators
- [x] Operation status
- [x] Loading states
- [x] Completion feedback
- [x] Basic recovery options
- [x] Automatic recovery
- [x] Manual intervention
- [x] Data restoration

13
ios/.gitignore vendored
View File

@@ -11,16 +11,3 @@ capacitor-cordova-ios-plugins
# Generated Config files # Generated Config files
App/App/capacitor.config.json App/App/capacitor.config.json
App/App/config.xml App/App/config.xml
# User-specific Xcode files
App/App.xcodeproj/xcuserdata/*.xcuserdatad/
App/App.xcodeproj/*.xcuserstate
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
# Generated Icons from capacitor-assets (also Contents.json which is confusing; see BUILDING.md)
App/App/Assets.xcassets/AppIcon.appiconset
App/App/Assets.xcassets/Splash.imageset

View File

@@ -14,7 +14,7 @@
504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; }; 504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; }; 504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; }; 50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
97EF2DC6FD76C3643D680B8D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 90DCAFB4D8948F7A50C13800 /* Pods_App.framework */; }; A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
@@ -27,9 +27,9 @@
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; 504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; }; 50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
90DCAFB4D8948F7A50C13800 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; }; AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; }; AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; }; FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@@ -37,17 +37,17 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
97EF2DC6FD76C3643D680B8D /* Pods_App.framework in Frameworks */, A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
/* End PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
4B546315E668C7A13939F417 /* Frameworks */ = { 27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
90DCAFB4D8948F7A50C13800 /* Pods_App.framework */, AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */,
); );
name = Frameworks; name = Frameworks;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -57,8 +57,8 @@
children = ( children = (
504EC3061FED79650016851F /* App */, 504EC3061FED79650016851F /* App */,
504EC3051FED79650016851F /* Products */, 504EC3051FED79650016851F /* Products */,
BA325FFCDCE8D334E5C7AEBE /* Pods */, 7F8756D8B27F46E3366F6CEA /* Pods */,
4B546315E668C7A13939F417 /* Frameworks */, 27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@@ -85,13 +85,13 @@
path = App; path = App;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
BA325FFCDCE8D334E5C7AEBE /* Pods */ = { 7F8756D8B27F46E3366F6CEA /* Pods */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */, FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */,
E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */, AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */,
); );
path = Pods; name = Pods;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
/* End PBXGroup section */ /* End PBXGroup section */
@@ -101,13 +101,12 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */; buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */;
buildPhases = ( buildPhases = (
92977BEA1068CC097A57FC77 /* [CP] Check Pods Manifest.lock */, 6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */,
504EC3001FED79650016851F /* Sources */, 504EC3001FED79650016851F /* Sources */,
504EC3011FED79650016851F /* Frameworks */, 504EC3011FED79650016851F /* Frameworks */,
504EC3021FED79650016851F /* Resources */, 504EC3021FED79650016851F /* Resources */,
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */,
012076E8FFE4BF260A79B034 /* Fix Privacy Manifest */, 012076E8FFE4BF260A79B034 /* Fix Privacy Manifest */,
3525031ED1C96EF4CF6E9959 /* [CP] Embed Pods Frameworks */,
96A7EF592DF3366D00084D51 /* Fix Privacy Manifest */,
); );
buildRules = ( buildRules = (
); );
@@ -187,10 +186,28 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PROJECT_DIR}/app_privacy_manifest_fixer/fixer.sh\" \n"; shellScript = "\"${PROJECT_DIR}/app_privacy_manifest_fixer/fixer.sh\" ";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
3525031ED1C96EF4CF6E9959 /* [CP] Embed Pods Frameworks */ = { 6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-App-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
@@ -205,47 +222,6 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-frameworks.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-frameworks.sh\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
92977BEA1068CC097A57FC77 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-App-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
96A7EF592DF3366D00084D51 /* Fix Privacy Manifest */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "Fix Privacy Manifest";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "$PROJECT_DIR/app_privacy_manifest_fixer/fixer.sh\n";
};
/* End PBXShellScriptBuildPhase section */ /* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */
@@ -399,12 +375,11 @@
}; };
504EC3171FED79650016851F /* Debug */ = { 504EC3171FED79650016851F /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */; baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 26; CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_TEAM = GM3FS5JQPH;
ENABLE_APP_SANDBOX = NO; ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
INFOPLIST_FILE = App/Info.plist; INFOPLIST_FILE = App/Info.plist;
@@ -413,7 +388,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.5.1; MARKETING_VERSION = 0.4.7;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari; PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@@ -426,12 +401,11 @@
}; };
504EC3181FED79650016851F /* Release */ = { 504EC3181FED79650016851F /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */; baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 26; CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_TEAM = GM3FS5JQPH;
ENABLE_APP_SANDBOX = NO; ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
INFOPLIST_FILE = App/Info.plist; INFOPLIST_FILE = App/Info.plist;
@@ -440,7 +414,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.5.1; MARKETING_VERSION = 0.4.7;
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari; PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";

View File

@@ -1,6 +1,5 @@
import UIKit import UIKit
import Capacitor import Capacitor
import CapacitorCommunitySqlite
@UIApplicationMain @UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate { class AppDelegate: UIResponder, UIApplicationDelegate {
@@ -8,10 +7,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow? var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Initialize SQLite
//let sqlite = SQLite()
//sqlite.initialize()
// Override point for customization after application launch. // Override point for customization after application launch.
return true return true
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

View File

@@ -0,0 +1,14 @@
{
"images": [
{
"idiom": "universal",
"size": "1024x1024",
"filename": "AppIcon-512@2x.png",
"platform": "ios"
}
],
"info": {
"author": "xcode",
"version": 1
}
}

View File

@@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "splash-2732x2732-2.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "splash-2732x2732-1.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "splash-2732x2732.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -11,7 +11,6 @@ install! 'cocoapods', :disable_input_output_paths => true
def capacitor_pods def capacitor_pods
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios' pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios' pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorCommunitySqlite', :path => '../../node_modules/@capacitor-community/sqlite'
pod 'CapacitorMlkitBarcodeScanning', :path => '../../node_modules/@capacitor-mlkit/barcode-scanning' pod 'CapacitorMlkitBarcodeScanning', :path => '../../node_modules/@capacitor-mlkit/barcode-scanning'
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app' pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera' pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera'
@@ -27,9 +26,4 @@ end
post_install do |installer| post_install do |installer|
assertDeploymentTarget(installer) assertDeploymentTarget(installer)
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64'
end
end
end end

View File

@@ -5,10 +5,6 @@ PODS:
- Capacitor - Capacitor
- CapacitorCamera (6.1.2): - CapacitorCamera (6.1.2):
- Capacitor - Capacitor
- CapacitorCommunitySqlite (6.0.2):
- Capacitor
- SQLCipher
- ZIPFoundation
- CapacitorCordova (6.2.1) - CapacitorCordova (6.2.1)
- CapacitorFilesystem (6.0.3): - CapacitorFilesystem (6.0.3):
- Capacitor - Capacitor
@@ -77,18 +73,11 @@ PODS:
- nanopb/decode (2.30910.0) - nanopb/decode (2.30910.0)
- nanopb/encode (2.30910.0) - nanopb/encode (2.30910.0)
- PromisesObjC (2.4.0) - PromisesObjC (2.4.0)
- SQLCipher (4.9.0):
- SQLCipher/standard (= 4.9.0)
- SQLCipher/common (4.9.0)
- SQLCipher/standard (4.9.0):
- SQLCipher/common
- ZIPFoundation (0.9.19)
DEPENDENCIES: DEPENDENCIES:
- "Capacitor (from `../../node_modules/@capacitor/ios`)" - "Capacitor (from `../../node_modules/@capacitor/ios`)"
- "CapacitorApp (from `../../node_modules/@capacitor/app`)" - "CapacitorApp (from `../../node_modules/@capacitor/app`)"
- "CapacitorCamera (from `../../node_modules/@capacitor/camera`)" - "CapacitorCamera (from `../../node_modules/@capacitor/camera`)"
- "CapacitorCommunitySqlite (from `../../node_modules/@capacitor-community/sqlite`)"
- "CapacitorCordova (from `../../node_modules/@capacitor/ios`)" - "CapacitorCordova (from `../../node_modules/@capacitor/ios`)"
- "CapacitorFilesystem (from `../../node_modules/@capacitor/filesystem`)" - "CapacitorFilesystem (from `../../node_modules/@capacitor/filesystem`)"
- "CapacitorMlkitBarcodeScanning (from `../../node_modules/@capacitor-mlkit/barcode-scanning`)" - "CapacitorMlkitBarcodeScanning (from `../../node_modules/@capacitor-mlkit/barcode-scanning`)"
@@ -109,8 +98,6 @@ SPEC REPOS:
- MLKitVision - MLKitVision
- nanopb - nanopb
- PromisesObjC - PromisesObjC
- SQLCipher
- ZIPFoundation
EXTERNAL SOURCES: EXTERNAL SOURCES:
Capacitor: Capacitor:
@@ -119,8 +106,6 @@ EXTERNAL SOURCES:
:path: "../../node_modules/@capacitor/app" :path: "../../node_modules/@capacitor/app"
CapacitorCamera: CapacitorCamera:
:path: "../../node_modules/@capacitor/camera" :path: "../../node_modules/@capacitor/camera"
CapacitorCommunitySqlite:
:path: "../../node_modules/@capacitor-community/sqlite"
CapacitorCordova: CapacitorCordova:
:path: "../../node_modules/@capacitor/ios" :path: "../../node_modules/@capacitor/ios"
CapacitorFilesystem: CapacitorFilesystem:
@@ -136,7 +121,6 @@ SPEC CHECKSUMS:
Capacitor: c95400d761e376be9da6be5a05f226c0e865cebf Capacitor: c95400d761e376be9da6be5a05f226c0e865cebf
CapacitorApp: e1e6b7d05e444d593ca16fd6d76f2b7c48b5aea7 CapacitorApp: e1e6b7d05e444d593ca16fd6d76f2b7c48b5aea7
CapacitorCamera: 9bc7b005d0e6f1d5f525b8137045b60cffffce79 CapacitorCamera: 9bc7b005d0e6f1d5f525b8137045b60cffffce79
CapacitorCommunitySqlite: 0299d20f4b00c2e6aa485a1d8932656753937b9b
CapacitorCordova: 8d93e14982f440181be7304aa9559ca631d77fff CapacitorCordova: 8d93e14982f440181be7304aa9559ca631d77fff
CapacitorFilesystem: 59270a63c60836248812671aa3b15df673fbaf74 CapacitorFilesystem: 59270a63c60836248812671aa3b15df673fbaf74
CapacitorMlkitBarcodeScanning: 7652be9c7922f39203a361de735d340ae37e134e CapacitorMlkitBarcodeScanning: 7652be9c7922f39203a361de735d340ae37e134e
@@ -154,9 +138,7 @@ SPEC CHECKSUMS:
MLKitVision: 90922bca854014a856f8b649d1f1f04f63fd9c79 MLKitVision: 90922bca854014a856f8b649d1f1f04f63fd9c79
nanopb: 438bc412db1928dac798aa6fd75726007be04262 nanopb: 438bc412db1928dac798aa6fd75726007be04262
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
SQLCipher: 31878d8ebd27e5c96db0b7cb695c96e9f8ad77da
ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c
PODFILE CHECKSUM: f987510f7383b04a1b09ea8472bdadcd88b6c924 PODFILE CHECKSUM: 7e7e09e6937de7f015393aecf2cf7823645689b3
COCOAPODS: 1.16.2 COCOAPODS: 1.16.2

2111
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "timesafari", "name": "timesafari",
"version": "0.5.1", "version": "0.4.6",
"description": "Time Safari Application", "description": "Time Safari Application",
"author": { "author": {
"name": "Time Safari Team" "name": "Time Safari Team"
@@ -46,7 +46,7 @@
"electron:build-mac-universal": "npm run build:electron-prod && electron-builder --mac --universal" "electron:build-mac-universal": "npm run build:electron-prod && electron-builder --mac --universal"
}, },
"dependencies": { "dependencies": {
"@capacitor-community/sqlite": "6.0.2", "@capacitor-community/sqlite": "6.0.0",
"@capacitor-mlkit/barcode-scanning": "^6.0.0", "@capacitor-mlkit/barcode-scanning": "^6.0.0",
"@capacitor/android": "^6.2.0", "@capacitor/android": "^6.2.0",
"@capacitor/app": "^6.0.0", "@capacitor/app": "^6.0.0",
@@ -116,6 +116,7 @@
"reflect-metadata": "^0.1.14", "reflect-metadata": "^0.1.14",
"register-service-worker": "^1.7.2", "register-service-worker": "^1.7.2",
"simple-vue-camera": "^1.1.3", "simple-vue-camera": "^1.1.3",
"sqlite": "^5.1.1",
"sqlite3": "^5.1.7", "sqlite3": "^5.1.7",
"stream-browserify": "^3.0.0", "stream-browserify": "^3.0.0",
"three": "^0.156.1", "three": "^0.156.1",
@@ -167,11 +168,12 @@
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"typescript": "~5.2.2", "typescript": "~5.2.2",
"vite": "^5.2.0", "vite": "^5.2.0",
"vite-plugin-pwa": "^1.0.0" "vite-plugin-node-polyfills": "^0.23.0",
"vite-plugin-pwa": "^0.19.8"
}, },
"main": "./dist-electron/main.js", "main": "./dist-electron/main.js",
"build": { "build": {
"appId": "app.timesafari.app", "appId": "app.timesafari",
"productName": "TimeSafari", "productName": "TimeSafari",
"directories": { "directories": {
"output": "dist-electron-packages" "output": "dist-electron-packages"
@@ -182,7 +184,7 @@
], ],
"extraResources": [ "extraResources": [
{ {
"from": "dist-electron/www", "from": "dist",
"to": "www" "to": "www"
} }
], ],

View File

@@ -2,6 +2,5 @@ dependencies:
- gradle - gradle
- java - java
- pod - pod
- rubygems.org
# other dependencies are discovered via package.json & requirements.txt & Gemfile (I'm guessing). # other dependencies are discovered via package.json & requirements.txt & Gemfile (I'm guessing).

Binary file not shown.

View File

@@ -1,6 +1,5 @@
eth_keys eth_keys
pywebview pywebview
pyinstaller>=6.12.0 pyinstaller>=6.12.0
setuptools>=69.0.0 # Required for distutils for electron-builder on macOS
# For development # For development
watchdog>=3.0.0 # For file watching support watchdog>=3.0.0 # For file watching support

View File

@@ -1,9 +1,10 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
console.log('Starting electron build process...'); console.log('Starting electron build process...');
// Define paths // Copy web files
const webDistPath = path.join(__dirname, '..', 'dist');
const electronDistPath = path.join(__dirname, '..', 'dist-electron'); const electronDistPath = path.join(__dirname, '..', 'dist-electron');
const wwwPath = path.join(electronDistPath, 'www'); const wwwPath = path.join(electronDistPath, 'www');
@@ -12,154 +13,231 @@ if (!fs.existsSync(wwwPath)) {
fs.mkdirSync(wwwPath, { recursive: true }); fs.mkdirSync(wwwPath, { recursive: true });
} }
// Create a platform-specific index.html for Electron // Copy web files to www directory
const initialIndexContent = `<!DOCTYPE html> fs.cpSync(webDistPath, wwwPath, { recursive: true });
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0,viewport-fit=cover">
<link rel="icon" href="/favicon.ico">
<title>TimeSafari</title>
</head>
<body>
<noscript>
<strong>We're sorry but TimeSafari doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<script type="module">
// Force electron platform
window.process = { env: { VITE_PLATFORM: 'electron' } };
import('./src/main.electron.ts');
</script>
</body>
</html>`;
// Write the Electron-specific index.html // Fix asset paths in index.html
fs.writeFileSync(path.join(wwwPath, 'index.html'), initialIndexContent);
// Copy only necessary assets from web build
const webDistPath = path.join(__dirname, '..', 'dist');
if (fs.existsSync(webDistPath)) {
// Copy assets directory
const assetsSrc = path.join(webDistPath, 'assets');
const assetsDest = path.join(wwwPath, 'assets');
if (fs.existsSync(assetsSrc)) {
fs.cpSync(assetsSrc, assetsDest, { recursive: true });
}
// Copy favicon
const faviconSrc = path.join(webDistPath, 'favicon.ico');
if (fs.existsSync(faviconSrc)) {
fs.copyFileSync(faviconSrc, path.join(wwwPath, 'favicon.ico'));
}
}
// Remove service worker files
const swFilesToRemove = [
'sw.js',
'sw.js.map',
'workbox-*.js',
'workbox-*.js.map',
'registerSW.js',
'manifest.webmanifest',
'**/workbox-*.js',
'**/workbox-*.js.map',
'**/sw.js',
'**/sw.js.map',
'**/registerSW.js',
'**/manifest.webmanifest'
];
console.log('Removing service worker files...');
swFilesToRemove.forEach(pattern => {
const files = fs.readdirSync(wwwPath).filter(file =>
file.match(new RegExp(pattern.replace(/\*/g, '.*')))
);
files.forEach(file => {
const filePath = path.join(wwwPath, file);
console.log(`Removing ${filePath}`);
try {
fs.unlinkSync(filePath);
} catch (err) {
console.warn(`Could not remove ${filePath}:`, err.message);
}
});
});
// Also check and remove from assets directory
const assetsPath = path.join(wwwPath, 'assets');
if (fs.existsSync(assetsPath)) {
swFilesToRemove.forEach(pattern => {
const files = fs.readdirSync(assetsPath).filter(file =>
file.match(new RegExp(pattern.replace(/\*/g, '.*')))
);
files.forEach(file => {
const filePath = path.join(assetsPath, file);
console.log(`Removing ${filePath}`);
try {
fs.unlinkSync(filePath);
} catch (err) {
console.warn(`Could not remove ${filePath}:`, err.message);
}
});
});
}
// Modify index.html to remove service worker registration
const indexPath = path.join(wwwPath, 'index.html'); const indexPath = path.join(wwwPath, 'index.html');
if (fs.existsSync(indexPath)) { let indexContent = fs.readFileSync(indexPath, 'utf8');
console.log('Modifying index.html to remove service worker registration...');
let indexContent = fs.readFileSync(indexPath, 'utf8');
// Remove service worker registration script
indexContent = indexContent
.replace(/<script[^>]*id="vite-plugin-pwa:register-sw"[^>]*><\/script>/g, '')
.replace(/<script[^>]*registerServiceWorker[^>]*><\/script>/g, '')
.replace(/<link[^>]*rel="manifest"[^>]*>/g, '')
.replace(/<link[^>]*rel="serviceworker"[^>]*>/g, '')
.replace(/navigator\.serviceWorker\.register\([^)]*\)/g, '')
.replace(/if\s*\(\s*['"]serviceWorker['"]\s*in\s*navigator\s*\)\s*{[^}]*}/g, '');
fs.writeFileSync(indexPath, indexContent);
console.log('Successfully modified index.html');
}
// Fix asset paths // Fix asset paths
console.log('Fixing asset paths in index.html...'); indexContent = indexContent
let modifiedIndexContent = fs.readFileSync(indexPath, 'utf8');
modifiedIndexContent = modifiedIndexContent
.replace(/\/assets\//g, './assets/') .replace(/\/assets\//g, './assets/')
.replace(/href="\//g, 'href="./') .replace(/href="\//g, 'href="./')
.replace(/src="\//g, 'src="./'); .replace(/src="\//g, 'src="./');
fs.writeFileSync(indexPath, modifiedIndexContent); fs.writeFileSync(indexPath, indexContent);
// Verify no service worker references remain
const finalContent = fs.readFileSync(indexPath, 'utf8');
if (finalContent.includes('serviceWorker') || finalContent.includes('workbox')) {
console.warn('Warning: Service worker references may still exist in index.html');
}
// Check for remaining /assets/ paths // Check for remaining /assets/ paths
console.log('After path fixing, checking for remaining /assets/ paths:', finalContent.includes('/assets/')); console.log('After path fixing, checking for remaining /assets/ paths:', indexContent.includes('/assets/'));
console.log('Sample of fixed content:', finalContent.substring(0, 500)); console.log('Sample of fixed content:', indexContent.substring(0, 500));
console.log('Copied and fixed web files in:', wwwPath); console.log('Copied and fixed web files in:', wwwPath);
// Copy main process files // Copy main process files
console.log('Copying main process files...'); console.log('Copying main process files...');
// Copy the main process file instead of creating a template // Create the main process file with inlined logger
const mainSrcPath = path.join(__dirname, '..', 'dist-electron', 'main.js'); const mainContent = `const { app, BrowserWindow } = require("electron");
const mainDestPath = path.join(electronDistPath, 'main.js'); const path = require("path");
const fs = require("fs");
if (fs.existsSync(mainSrcPath)) { // Inline logger implementation
fs.copyFileSync(mainSrcPath, mainDestPath); const logger = {
console.log('Copied main process file successfully'); log: (...args) => console.log(...args),
} else { error: (...args) => console.error(...args),
console.error('Main process file not found at:', mainSrcPath); info: (...args) => console.info(...args),
process.exit(1); warn: (...args) => console.warn(...args),
debug: (...args) => console.debug(...args),
};
// Check if running in dev mode
const isDev = process.argv.includes("--inspect");
function createWindow() {
// Add before createWindow function
const preloadPath = path.join(__dirname, "preload.js");
logger.log("Checking preload path:", preloadPath);
logger.log("Preload exists:", fs.existsSync(preloadPath));
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
webSecurity: true,
allowRunningInsecureContent: false,
preload: path.join(__dirname, "preload.js"),
},
});
// Always open DevTools for now
mainWindow.webContents.openDevTools();
// Intercept requests to fix asset paths
mainWindow.webContents.session.webRequest.onBeforeRequest(
{
urls: [
"file://*/*/assets/*",
"file://*/assets/*",
"file:///assets/*", // Catch absolute paths
"<all_urls>", // Catch all URLs as a fallback
],
},
(details, callback) => {
let url = details.url;
// Handle paths that don't start with file://
if (!url.startsWith("file://") && url.includes("/assets/")) {
url = \`file://\${path.join(__dirname, "www", url)}\`;
}
// Handle absolute paths starting with /assets/
if (url.includes("/assets/") && !url.includes("/www/assets/")) {
const baseDir = url.includes("dist-electron")
? url.substring(
0,
url.indexOf("/dist-electron") + "/dist-electron".length,
)
: \`file://\${__dirname}\`;
const assetPath = url.split("/assets/")[1];
const newUrl = \`\${baseDir}/www/assets/\${assetPath}\`;
callback({ redirectURL: newUrl });
return;
}
callback({}); // No redirect for other URLs
},
);
if (isDev) {
// Debug info
logger.log("Debug Info:");
logger.log("Running in dev mode:", isDev);
logger.log("App is packaged:", app.isPackaged);
logger.log("Process resource path:", process.resourcesPath);
logger.log("App path:", app.getAppPath());
logger.log("__dirname:", __dirname);
logger.log("process.cwd():", process.cwd());
}
const indexPath = path.join(__dirname, "www", "index.html");
if (isDev) {
logger.log("Loading index from:", indexPath);
logger.log("www path:", path.join(__dirname, "www"));
logger.log("www assets path:", path.join(__dirname, "www", "assets"));
}
if (!fs.existsSync(indexPath)) {
logger.error(\`Index file not found at: \${indexPath}\`);
throw new Error("Index file not found");
}
// Add CSP headers to allow API connections, Google Fonts, and zxing-wasm
mainWindow.webContents.session.webRequest.onHeadersReceived(
(details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
"Content-Security-Policy": [
"default-src 'self';" +
"connect-src 'self' https://api.endorser.ch https://*.timesafari.app https://*.jsdelivr.net;" +
"img-src 'self' data: https: blob:;" +
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://*.jsdelivr.net;" +
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;" +
"font-src 'self' data: https://fonts.gstatic.com;" +
"style-src-elem 'self' 'unsafe-inline' https://fonts.googleapis.com;" +
"worker-src 'self' blob:;",
],
},
});
},
);
// Load the index.html
mainWindow
.loadFile(indexPath)
.then(() => {
logger.log("Successfully loaded index.html");
if (isDev) {
mainWindow.webContents.openDevTools();
logger.log("DevTools opened - running in dev mode");
}
})
.catch((err) => {
logger.error("Failed to load index.html:", err);
logger.error("Attempted path:", indexPath);
});
// Listen for console messages from the renderer
mainWindow.webContents.on("console-message", (_event, _level, message) => {
logger.log("Renderer Console:", message);
});
// Add right after creating the BrowserWindow
mainWindow.webContents.on(
"did-fail-load",
(_event, errorCode, errorDescription) => {
logger.error("Page failed to load:", errorCode, errorDescription);
},
);
mainWindow.webContents.on("preload-error", (_event, preloadPath, error) => {
logger.error("Preload script error:", preloadPath, error);
});
mainWindow.webContents.on(
"console-message",
(_event, _level, message, line, sourceId) => {
logger.log("Renderer Console:", line, sourceId, message);
},
);
// Enable remote debugging when in dev mode
if (isDev) {
mainWindow.webContents.openDevTools();
}
} }
console.log('Electron build process completed successfully'); // Handle app ready
app.whenReady().then(createWindow);
// Handle all windows closed
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// Handle any errors
process.on("uncaughtException", (error) => {
logger.error("Uncaught Exception:", error);
});
`;
// Write the main process file
const mainDest = path.join(electronDistPath, 'main.js');
fs.writeFileSync(mainDest, mainContent);
// Copy preload script if it exists
const preloadSrc = path.join(__dirname, '..', 'src', 'electron', 'preload.js');
const preloadDest = path.join(electronDistPath, 'preload.js');
if (fs.existsSync(preloadSrc)) {
console.log(`Copying ${preloadSrc} to ${preloadDest}`);
fs.copyFileSync(preloadSrc, preloadDest);
}
// Verify build structure
console.log('\nVerifying build structure:');
console.log('Files in dist-electron:', fs.readdirSync(electronDistPath));
console.log('Build completed successfully!');

View File

@@ -51,7 +51,7 @@ const { existsSync } = require('fs');
*/ */
function checkCommand(command, errorMessage) { function checkCommand(command, errorMessage) {
try { try {
execSync(command, { stdio: 'ignore' }); execSync(command + ' --version', { stdio: 'ignore' });
return true; return true;
} catch (e) { } catch (e) {
console.error(`${errorMessage}`); console.error(`${errorMessage}`);
@@ -164,10 +164,10 @@ function main() {
// Check required command line tools // Check required command line tools
// These are essential for building and testing the application // These are essential for building and testing the application
success &= checkCommand('node --version', 'Node.js is required'); success &= checkCommand('node', 'Node.js is required');
success &= checkCommand('npm --version', 'npm is required'); success &= checkCommand('npm', 'npm is required');
success &= checkCommand('gradle --version', 'Gradle is required for Android builds'); success &= checkCommand('gradle', 'Gradle is required for Android builds');
success &= checkCommand('xcodebuild --help', 'Xcode is required for iOS builds'); success &= checkCommand('xcodebuild', 'Xcode is required for iOS builds');
// Check platform-specific development environments // Check platform-specific development environments
success &= checkAndroidSetup(); success &= checkAndroidSetup();

View File

@@ -170,7 +170,7 @@ const executeDeeplink = async (url, description, log) => {
try { try {
// Stop the app before executing the deep link // Stop the app before executing the deep link
execSync('adb shell am force-stop app.timesafari.app'); execSync('adb shell am force-stop app.timesafari');
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1s await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1s
execSync(`adb shell am start -W -a android.intent.action.VIEW -d "${url}" -c android.intent.category.BROWSABLE`); execSync(`adb shell am start -W -a android.intent.action.VIEW -d "${url}" -c android.intent.category.BROWSABLE`);

View File

@@ -4,7 +4,7 @@
<!-- Messages in the upper-right - https://github.com/emmanuelsw/notiwind --> <!-- Messages in the upper-right - https://github.com/emmanuelsw/notiwind -->
<NotificationGroup group="alert"> <NotificationGroup group="alert">
<div <div
class="fixed z-[90] top-[max(1rem,env(safe-area-inset-top))] right-4 left-4 sm:left-auto sm:w-full sm:max-w-sm flex flex-col items-start justify-end" class="fixed top-[calc(env(safe-area-inset-top)+1rem)] right-4 left-4 sm:left-auto sm:w-full sm:max-w-sm flex flex-col items-start justify-end"
> >
<Notification <Notification
v-slot="{ notifications, close }" v-slot="{ notifications, close }"
@@ -330,11 +330,8 @@
<script lang="ts"> <script lang="ts">
import { Vue, Component } from "vue-facing-decorator"; import { Vue, Component } from "vue-facing-decorator";
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from "./db/index";
import { NotificationIface, USE_DEXIE_DB } from "./constants/app"; import { NotificationIface } from "./constants/app";
import * as databaseUtil from "./db/databaseUtil";
import { retrieveSettingsForActiveAccount } from "./db/index";
import { logConsoleAndDb } from "./db/databaseUtil";
import { logger } from "./utils/logger"; import { logger } from "./utils/logger";
interface Settings { interface Settings {
@@ -399,11 +396,7 @@ export default class App extends Vue {
try { try {
logger.log("Retrieving settings for the active account..."); logger.log("Retrieving settings for the active account...");
let settings: Settings = const settings: Settings = await retrieveSettingsForActiveAccount();
await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
logger.log("Retrieved settings:", settings); logger.log("Retrieved settings:", settings);
const notifyingNewActivity = !!settings?.notifyingNewActivityTime; const notifyingNewActivity = !!settings?.notifyingNewActivityTime;
@@ -548,13 +541,13 @@ export default class App extends Vue {
<style> <style>
#Content { #Content {
padding-left: max(1.5rem, env(safe-area-inset-left)); padding-left: 1.5rem;
padding-right: max(1.5rem, env(safe-area-inset-right)); padding-right: 1.5rem;
padding-top: max(1.5rem, env(safe-area-inset-top)); padding-top: calc(env(safe-area-inset-top) + 1.5rem);
padding-bottom: max(1.5rem, env(safe-area-inset-bottom)); padding-bottom: calc(env(safe-area-inset-bottom) + 1.5rem);
} }
#QuickNav ~ #Content { #QuickNav ~ #Content {
padding-bottom: calc(env(safe-area-inset-bottom) + 6.333rem); padding-bottom: calc(env(safe-area-inset-bottom) + 6rem);
} }
</style> </style>

View File

@@ -14,34 +14,22 @@
class="flex items-center justify-between gap-2 text-lg bg-slate-200 border border-slate-300 border-b-0 rounded-t-md px-3 sm:px-4 py-1 sm:py-2" class="flex items-center justify-between gap-2 text-lg bg-slate-200 border border-slate-300 border-b-0 rounded-t-md px-3 sm:px-4 py-1 sm:py-2"
> >
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<router-link <div v-if="record.issuerDid">
v-if="record.issuerDid && !isHiddenDid(record.issuerDid)"
:to="{
path: '/did/' + encodeURIComponent(record.issuerDid),
}"
title="More details about this person"
>
<EntityIcon <EntityIcon
:entity-id="record.issuerDid" :entity-id="record.issuerDid"
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover" class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
/> />
</router-link> </div>
<font-awesome <div v-else>
v-else-if="isHiddenDid(record.issuerDid)" <font-awesome
icon="eye-slash" icon="person-circle-question"
class="text-slate-400 !size-[2rem] cursor-pointer" class="text-slate-300 text-[2rem]"
@click="notifyHiddenPerson" />
/> </div>
<font-awesome
v-else
icon="person-circle-question"
class="text-slate-400 !size-[2rem] cursor-pointer"
@click="notifyUnknownPerson"
/>
<div> <div>
<h3 v-if="record.issuer.known" class="font-semibold leading-tight"> <h3 class="font-semibold">
{{ record.issuer.displayName }} {{ record.issuer.known ? record.issuer.displayName : "" }}
</h3> </h3>
<p class="ms-auto text-xs text-slate-500 italic"> <p class="ms-auto text-xs text-slate-500 italic">
{{ friendlyDate }} {{ friendlyDate }}
@@ -49,11 +37,7 @@
</div> </div>
</div> </div>
<a <a class="cursor-pointer" @click="$emit('loadClaim', record.jwtId)">
class="cursor-pointer"
data-testid="circle-info-link"
@click="$emit('loadClaim', record.jwtId)"
>
<font-awesome icon="circle-info" class="fa-fw text-slate-500" /> <font-awesome icon="circle-info" class="fa-fw text-slate-500" />
</a> </a>
</div> </div>
@@ -62,7 +46,7 @@
<!-- Record Image --> <!-- Record Image -->
<div <div
v-if="record.image" v-if="record.image"
class="bg-cover mb-2 -mt-3 sm:-mt-4 -mx-3 sm:-mx-4" class="bg-cover mb-6 -mt-3 sm:-mt-4 -mx-3 sm:-mx-4"
:style="`background-image: url(${record.image});`" :style="`background-image: url(${record.image});`"
> >
<a <a
@@ -78,59 +62,29 @@
</a> </a>
</div> </div>
<!-- Description -->
<p class="font-medium">
<a class="cursor-pointer" @click="$emit('loadClaim', record.jwtId)">
{{ description }}
</a>
</p>
<div <div
class="relative flex justify-between gap-4 max-w-[40rem] mx-auto mt-4" class="relative flex justify-between gap-4 max-w-[40rem] mx-auto mb-5"
> >
<!-- Source --> <!-- Source -->
<div <div
class="w-[7rem] sm:w-[12rem] text-center bg-white border border-slate-200 rounded p-2 sm:p-3" class="w-[8rem] sm:w-[12rem] text-center bg-white border border-slate-200 rounded p-2 sm:p-3"
> >
<div class="relative w-fit mx-auto"> <div class="relative w-fit mx-auto">
<div> <div>
<!-- Project Icon --> <!-- Project Icon -->
<div v-if="record.providerPlanName"> <div v-if="record.providerPlanName">
<router-link <ProjectIcon
:to="{ :entity-id="record.providerPlanName"
path: :icon-size="48"
'/project/' + class="rounded size-[3rem] sm:size-[4rem] *:w-full *:h-full"
encodeURIComponent(record.providerPlanHandleId || ''), />
}"
title="View project details"
>
<ProjectIcon
:entity-id="record.providerPlanHandleId || ''"
:icon-size="48"
class="rounded size-[3rem] sm:size-[4rem] *:w-full *:h-full"
/>
</router-link>
</div> </div>
<!-- Identicon for DIDs --> <!-- Identicon for DIDs -->
<div v-else-if="record.agentDid"> <div v-else-if="record.agentDid">
<router-link <EntityIcon
v-if="!isHiddenDid(record.agentDid)" :entity-id="record.agentDid"
:to="{ :profile-image-url="record.issuer.profileImageUrl"
path: '/did/' + encodeURIComponent(record.agentDid), class="rounded-full bg-slate-100 overflow-hidden !size-[3rem] sm:!size-[4rem]"
}"
title="More details about this person"
>
<EntityIcon
:entity-id="record.agentDid"
:profile-image-url="record.issuer.profileImageUrl"
class="rounded-full bg-slate-100 overflow-hidden !size-[3rem] sm:!size-[4rem]"
/>
</router-link>
<font-awesome
v-else
icon="eye-slash"
class="text-slate-300 !size-[3rem] sm:!size-[4rem]"
@click="notifyHiddenPerson"
/> />
</div> </div>
<!-- Unknown Person --> <!-- Unknown Person -->
@@ -138,7 +92,6 @@
<font-awesome <font-awesome
icon="person-circle-question" icon="person-circle-question"
class="text-slate-300 text-[3rem] sm:text-[4rem]" class="text-slate-300 text-[3rem] sm:text-[4rem]"
@click="notifyUnknownPerson"
/> />
</div> </div>
</div> </div>
@@ -157,11 +110,9 @@
<!-- Arrow --> <!-- Arrow -->
<div <div
class="absolute inset-x-[7rem] sm:inset-x-[12rem] mx-2 top-1/2 -translate-y-1/2" class="absolute inset-x-[8rem] sm:inset-x-[12rem] mx-2 top-1/2 -translate-y-1/2"
> >
<div <div class="text-sm text-center leading-none font-semibold pe-[15px]">
class="text-sm text-center leading-none font-semibold pe-2 sm:pe-4"
>
{{ fetchAmount }} {{ fetchAmount }}
</div> </div>
@@ -178,47 +129,24 @@
<!-- Destination --> <!-- Destination -->
<div <div
class="w-[7rem] sm:w-[12rem] text-center bg-white border border-slate-200 rounded p-2 sm:p-3" class="w-[8rem] sm:w-[12rem] text-center bg-white border border-slate-200 rounded p-2 sm:p-3"
> >
<div class="relative w-fit mx-auto"> <div class="relative w-fit mx-auto">
<div> <div>
<!-- Project Icon --> <!-- Project Icon -->
<div v-if="record.recipientProjectName"> <div v-if="record.recipientProjectName">
<router-link <ProjectIcon
:to="{ :entity-id="record.recipientProjectName"
path: :icon-size="48"
'/project/' + class="rounded size-[3rem] sm:size-[4rem] *:w-full *:h-full"
encodeURIComponent(record.fulfillsPlanHandleId || ''), />
}"
title="View project details"
>
<ProjectIcon
:entity-id="record.fulfillsPlanHandleId || ''"
:icon-size="48"
class="rounded size-[3rem] sm:size-[4rem] *:w-full *:h-full"
/>
</router-link>
</div> </div>
<!-- Identicon for DIDs --> <!-- Identicon for DIDs -->
<div v-else-if="record.recipientDid"> <div v-else-if="record.recipientDid">
<router-link <EntityIcon
v-if="!isHiddenDid(record.recipientDid)" :entity-id="record.recipientDid"
:to="{ :profile-image-url="record.receiver.profileImageUrl"
path: '/did/' + encodeURIComponent(record.recipientDid), class="rounded-full bg-slate-100 overflow-hidden !size-[3rem] sm:!size-[4rem]"
}"
title="More details about this person"
>
<EntityIcon
:entity-id="record.recipientDid"
:profile-image-url="record.receiver.profileImageUrl"
class="rounded-full bg-slate-100 overflow-hidden !size-[3rem] sm:!size-[4rem]"
/>
</router-link>
<font-awesome
v-else
icon="eye-slash"
class="text-slate-300 !size-[3rem] sm:!size-[4rem]"
@click="notifyHiddenPerson"
/> />
</div> </div>
<!-- Unknown Person --> <!-- Unknown Person -->
@@ -226,7 +154,6 @@
<font-awesome <font-awesome
icon="person-circle-question" icon="person-circle-question"
class="text-slate-300 text-[3rem] sm:text-[4rem]" class="text-slate-300 text-[3rem] sm:text-[4rem]"
@click="notifyUnknownPerson"
/> />
</div> </div>
</div> </div>
@@ -243,6 +170,13 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Description -->
<p class="font-medium">
<a class="cursor-pointer" @click="$emit('loadClaim', record.jwtId)">
{{ description }}
</a>
</p>
</div> </div>
</li> </li>
</template> </template>
@@ -252,9 +186,8 @@ import { Component, Prop, Vue, Emit } from "vue-facing-decorator";
import { GiveRecordWithContactInfo } from "../types"; import { GiveRecordWithContactInfo } from "../types";
import EntityIcon from "./EntityIcon.vue"; import EntityIcon from "./EntityIcon.vue";
import { isGiveClaimType, notifyWhyCannotConfirm } from "../libs/util"; import { isGiveClaimType, notifyWhyCannotConfirm } from "../libs/util";
import { containsHiddenDid, isHiddenDid } from "../libs/endorserServer"; import { containsHiddenDid } from "../libs/endorserServer";
import ProjectIcon from "./ProjectIcon.vue"; import ProjectIcon from "./ProjectIcon.vue";
import { NotificationIface } from "../constants/app";
@Component({ @Component({
components: { components: {
@@ -269,33 +202,6 @@ export default class ActivityListItem extends Vue {
@Prop() activeDid!: string; @Prop() activeDid!: string;
@Prop() confirmerIdList?: string[]; @Prop() confirmerIdList?: string[];
isHiddenDid = isHiddenDid;
$notify!: (notification: NotificationIface, timeout?: number) => void;
notifyHiddenPerson() {
this.$notify(
{
group: "alert",
type: "warning",
title: "Person Outside Your Network",
text: "This person is not visible to you.",
},
3000,
);
}
notifyUnknownPerson() {
this.$notify(
{
group: "alert",
type: "warning",
title: "Unidentified Person",
text: "Nobody specific was recognized.",
},
3000,
);
}
@Emit() @Emit()
cacheImage(image: string) { cacheImage(image: string) {
return image; return image;
@@ -316,7 +222,7 @@ export default class ActivityListItem extends Vue {
const claim = const claim =
(this.record.fullClaim as unknown).claim || this.record.fullClaim; (this.record.fullClaim as unknown).claim || this.record.fullClaim;
return `${claim?.description || ""}`; return `${claim.description}`;
} }
private displayAmount(code: string, amt: number) { private displayAmount(code: string, amt: number) {

View File

@@ -1,207 +0,0 @@
/** * AmountInput.vue - Specialized amount input with increment/decrement
controls * * Extracted from GiftedDialog.vue to handle numeric amount input *
with increment/decrement buttons and validation. * * @author Matthew Raymer */
<template>
<div class="flex flex-grow">
<button
class="rounded-s border border-e-0 border-slate-400 bg-slate-200 px-4 py-2"
:disabled="isAtMinimum"
type="button"
@click.prevent="decrement"
>
<font-awesome icon="chevron-left" />
</button>
<input
:id="inputId"
v-model="displayValue"
type="number"
:min="min"
:max="max"
:step="step"
class="flex-1 border border-e-0 border-slate-400 px-2 py-2 text-center w-[1px]"
@input="handleInput"
@blur="handleBlur"
/>
<button
class="rounded-e border border-slate-400 bg-slate-200 px-4 py-2"
:disabled="isAtMaximum"
type="button"
@click.prevent="increment"
>
<font-awesome icon="chevron-right" />
</button>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch, Emit } from "vue-facing-decorator";
/**
* AmountInput - Numeric input with increment/decrement controls
*
* Features:
* - Increment/decrement buttons with validation
* - Configurable min/max values and step size
* - Input validation and formatting
* - Disabled state handling for boundary values
* - Emits update events for v-model compatibility
*/
@Component
export default class AmountInput extends Vue {
/** Current numeric value */
@Prop({ required: true })
value!: number;
/** Minimum allowed value */
@Prop({ default: 0 })
min!: number;
/** Maximum allowed value */
@Prop({ default: Number.MAX_SAFE_INTEGER })
max!: number;
/** Step size for increment/decrement */
@Prop({ default: 1 })
step!: number;
/** Input element ID for accessibility */
@Prop({ default: "amount-input" })
inputId!: string;
/** Internal display value for input field */
private displayValue: string = "0";
/**
* Initialize display value from prop
*/
mounted(): void {
console.log(
`[AmountInput] mounted() - initial value: ${this.value}, min: ${this.min}, max: ${this.max}, step: ${this.step}`,
);
this.displayValue = this.value.toString();
console.log(
`[AmountInput] mounted() - displayValue set to: ${this.displayValue}`,
);
}
/**
* Watch for external value changes
*/
@Watch("value")
onValueChange(newValue: number): void {
this.displayValue = newValue.toString();
}
/**
* Check if current value is at minimum
*/
get isAtMinimum(): boolean {
const result = this.value <= this.min;
console.log(
`[AmountInput] isAtMinimum - value: ${this.value}, min: ${this.min}, result: ${result}`,
);
return result;
}
/**
* Check if current value is at maximum
*/
get isAtMaximum(): boolean {
const result = this.value >= this.max;
console.log(
`[AmountInput] isAtMaximum - value: ${this.value}, max: ${this.max}, result: ${result}`,
);
return result;
}
/**
* Increment the value by step size
*/
increment(): void {
console.log(
`[AmountInput] increment() called - current value: ${this.value}, step: ${this.step}`,
);
const newValue = Math.min(this.value + this.step, this.max);
console.log(`[AmountInput] increment() calculated newValue: ${newValue}`);
this.updateValue(newValue);
}
/**
* Decrement the value by step size
*/
decrement(): void {
console.log(
`[AmountInput] decrement() called - current value: ${this.value}, step: ${this.step}`,
);
const newValue = Math.max(this.value - this.step, this.min);
console.log(`[AmountInput] decrement() calculated newValue: ${newValue}`);
this.updateValue(newValue);
}
/**
* Handle direct input changes
*/
handleInput(): void {
const numericValue = parseFloat(this.displayValue);
if (!isNaN(numericValue)) {
const clampedValue = Math.max(this.min, Math.min(numericValue, this.max));
this.updateValue(clampedValue);
}
}
/**
* Handle input blur - ensure display value matches actual value
*/
handleBlur(): void {
this.displayValue = this.value.toString();
}
/**
* Update the value and emit change event
*/
private updateValue(newValue: number): void {
console.log(
`[AmountInput] updateValue() called - oldValue: ${this.value}, newValue: ${newValue}`,
);
if (newValue !== this.value) {
console.log(
`[AmountInput] updateValue() - values different, updating and emitting`,
);
this.displayValue = newValue.toString();
this.emitUpdateValue(newValue);
} else {
console.log(`[AmountInput] updateValue() - values same, skipping update`);
}
}
/**
* Emit update:value event
*/
@Emit("update:value")
emitUpdateValue(value: number): number {
console.log(`[AmountInput] emitUpdateValue() - emitting value: ${value}`);
return value;
}
}
</script>
<style scoped>
/* Remove spinner arrows from number input */
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type="number"] {
-moz-appearance: textfield;
}
/* Disabled button styles */
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>

View File

@@ -24,7 +24,9 @@ backup and database export, with platform-specific download instructions. * *
class="block w-full text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md" class="block w-full text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
@click="exportDatabase()" @click="exportDatabase()"
> >
Download Contacts Download Settings & Contacts
<br />
(excluding Identifier Data)
</button> </button>
<a <a
ref="downloadLink" ref="downloadLink"
@@ -60,18 +62,14 @@ backup and database export, with platform-specific download instructions. * *
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from "vue-facing-decorator"; import { Component, Prop, Vue } from "vue-facing-decorator";
import { AppString, NotificationIface } from "../constants/app"; import { NotificationIface } from "../constants/app";
import { db } from "../db/index";
import { Contact } from "../db/tables/contacts";
import * as databaseUtil from "../db/databaseUtil";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "../services/PlatformServiceFactory"; import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
import { import {
PlatformService, PlatformService,
PlatformCapabilities, PlatformCapabilities,
} from "../services/PlatformService"; } from "../services/PlatformService";
import { contactsToExportJson } from "../libs/util";
/** /**
* @vue-component * @vue-component
@@ -133,25 +131,21 @@ export default class DataExportSection extends Vue {
*/ */
public async exportDatabase() { public async exportDatabase() {
try { try {
let allContacts: Contact[] = []; const blob = await db.export({
const platformService = PlatformServiceFactory.getInstance(); prettyJson: true,
const result = await platformService.dbQuery(`SELECT * FROM contacts`); transform: (table, value, key) => {
if (result) { if (table === "contacts") {
allContacts = databaseUtil.mapQueryResultToValues( // Dexie inserts a number 0 when some are undefined, so we need to totally remove them.
result, Object.keys(value).forEach((prop) => {
) as unknown as Contact[]; if (value[prop] === undefined) {
} delete value[prop];
// if (USE_DEXIE_DB) { }
// await db.open(); });
// allContacts = await db.contacts.toArray(); }
// } return { value, key };
},
// Convert contacts to export format });
const exportData = contactsToExportJson(allContacts); const fileName = `${db.name}-backup.json`;
const jsonStr = JSON.stringify(exportData, null, 2);
const blob = new Blob([jsonStr], { type: "application/json" });
const fileName = `${AppString.APP_NAME_NO_SPACES}-backup-contacts.json`;
if (this.platformCapabilities.hasFileDownload) { if (this.platformCapabilities.hasFileDownload) {
// Web platform: Use download link // Web platform: Use download link
@@ -163,9 +157,8 @@ export default class DataExportSection extends Vue {
setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000); setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000);
} else if (this.platformCapabilities.hasFileSystem) { } else if (this.platformCapabilities.hasFileSystem) {
// Native platform: Write to app directory // Native platform: Write to app directory
await this.platformService.writeAndShareFile(fileName, jsonStr); const content = await blob.text();
} else { await this.platformService.writeAndShareFile(fileName, content);
throw new Error("This platform does not support file downloads.");
} }
this.$notify( this.$notify(
@@ -174,10 +167,10 @@ export default class DataExportSection extends Vue {
type: "success", type: "success",
title: "Export Successful", title: "Export Successful",
text: this.platformCapabilities.hasFileDownload text: this.platformCapabilities.hasFileDownload
? "See your downloads directory for the backup." ? "See your downloads directory for the backup. It is in the Dexie format."
: "The backup file has been saved.", : "You should have been prompted to save your backup file.",
}, },
3000, -1,
); );
} catch (error) { } catch (error) {
logger.error("Export Error:", error); logger.error("Export Error:", error);

View File

@@ -1,264 +0,0 @@
/** * EntityGrid.vue - Unified entity grid layout component * * Extracted from
GiftedDialog.vue to provide a reusable grid layout * for displaying people,
projects, and special entities with selection. * * @author Matthew Raymer */
<template>
<ul :class="gridClasses">
<!-- Special entities (You, Unnamed) for people grids -->
<template v-if="entityType === 'people'">
<!-- "You" entity -->
<SpecialEntityCard
v-if="showYouEntity"
entity-type="you"
label="You"
icon="hand"
:selectable="youSelectable"
:conflicted="youConflicted"
:entity-data="youEntityData"
@entity-selected="handleEntitySelected"
/>
<!-- "Unnamed" entity -->
<SpecialEntityCard
entity-type="unnamed"
label="Unnamed"
icon="circle-question"
:entity-data="unnamedEntityData"
@entity-selected="handleEntitySelected"
/>
</template>
<!-- Empty state message -->
<li
v-if="entities.length === 0"
class="text-xs text-slate-500 italic col-span-full"
>
{{ emptyStateMessage }}
</li>
<!-- Entity cards (people or projects) -->
<template v-if="entityType === 'people'">
<PersonCard
v-for="person in displayedEntities"
:key="person.did"
:person="person"
:conflicted="isPersonConflicted(person.did)"
:show-time-icon="true"
@person-selected="handlePersonSelected"
/>
</template>
<template v-else-if="entityType === 'projects'">
<ProjectCard
v-for="project in displayedEntities"
:key="project.handleId"
:project="project"
:active-did="activeDid"
:all-my-dids="allMyDids"
:all-contacts="allContacts"
@project-selected="handleProjectSelected"
/>
</template>
<!-- Show All navigation -->
<ShowAllCard
v-if="shouldShowAll"
:entity-type="entityType"
:route-name="showAllRoute"
:query-params="showAllQueryParams"
/>
</ul>
</template>
<script lang="ts">
import { Component, Prop, Vue, Emit } from "vue-facing-decorator";
import PersonCard from "./PersonCard.vue";
import ProjectCard from "./ProjectCard.vue";
import SpecialEntityCard from "./SpecialEntityCard.vue";
import ShowAllCard from "./ShowAllCard.vue";
import { Contact } from "../db/tables/contacts";
import { PlanData } from "../interfaces/records";
/**
* EntityGrid - Unified grid layout for displaying people or projects
*
* Features:
* - Responsive grid layout for people/projects
* - Special entity integration (You, Unnamed)
* - Conflict detection integration
* - Empty state messaging
* - Show All navigation
* - Event delegation for entity selection
*/
@Component({
components: {
PersonCard,
ProjectCard,
SpecialEntityCard,
ShowAllCard,
},
})
export default class EntityGrid extends Vue {
/** Type of entities to display */
@Prop({ required: true })
entityType!: "people" | "projects";
/** Array of entities to display */
@Prop({ required: true })
entities!: Contact[] | PlanData[];
/** Maximum number of entities to display */
@Prop({ default: 10 })
maxItems!: number;
/** Active user's DID */
@Prop({ required: true })
activeDid!: string;
/** All user's DIDs */
@Prop({ required: true })
allMyDids!: string[];
/** All contacts */
@Prop({ required: true })
allContacts!: Contact[];
/** Function to check if a person DID would create a conflict */
@Prop({ required: true })
conflictChecker!: (did: string) => boolean;
/** Whether to show the "You" entity for people grids */
@Prop({ default: true })
showYouEntity!: boolean;
/** Whether the "You" entity is selectable */
@Prop({ default: true })
youSelectable!: boolean;
/** Route name for "Show All" navigation */
@Prop({ default: "" })
showAllRoute!: string;
/** Query parameters for "Show All" navigation */
@Prop({ default: () => ({}) })
showAllQueryParams!: Record<string, any>;
/**
* Computed CSS classes for the grid layout
*/
get gridClasses(): string {
const baseClasses = "grid gap-x-2 gap-y-4 text-center mb-4";
if (this.entityType === "projects") {
return `${baseClasses} grid-cols-3 md:grid-cols-4`;
} else {
return `${baseClasses} grid-cols-4 sm:grid-cols-5 md:grid-cols-6`;
}
}
/**
* Computed entities to display (limited by maxItems)
*/
get displayedEntities(): Contact[] | PlanData[] {
const maxDisplay = this.entityType === "projects" ? 7 : this.maxItems;
return this.entities.slice(0, maxDisplay);
}
/**
* Computed empty state message based on entity type
*/
get emptyStateMessage(): string {
if (this.entityType === "projects") {
return "(No projects found.)";
} else {
return "(Add friends to see more people worthy of recognition.)";
}
}
/**
* Whether to show the "Show All" navigation
*/
get shouldShowAll(): boolean {
return this.entities.length > 0 && this.showAllRoute !== "";
}
/**
* Whether the "You" entity is conflicted
*/
get youConflicted(): boolean {
return this.conflictChecker(this.activeDid);
}
/**
* Entity data for the "You" special entity
*/
get youEntityData(): { did: string; name: string } {
return {
did: this.activeDid,
name: "You",
};
}
/**
* Entity data for the "Unnamed" special entity
*/
get unnamedEntityData(): { did: string; name: string } {
return {
did: "",
name: "Unnamed",
};
}
/**
* Check if a person DID is conflicted
*/
isPersonConflicted(did: string): boolean {
return this.conflictChecker(did);
}
/**
* Handle person selection from PersonCard
*/
handlePersonSelected(person: Contact): void {
this.emitEntitySelected({
type: "person",
data: person,
});
}
/**
* Handle project selection from ProjectCard
*/
handleProjectSelected(project: PlanData): void {
this.emitEntitySelected({
type: "project",
data: project,
});
}
/**
* Handle special entity selection from SpecialEntityCard
*/
handleEntitySelected(event: {
type: string;
entityType: string;
data: any;
}): void {
this.emitEntitySelected({
type: "special",
entityType: event.entityType,
data: event.data,
});
}
// Emit methods using @Emit decorator
@Emit("entity-selected")
emitEntitySelected(data: any): any {
return data;
}
}
</script>
<style scoped>
/* Grid-specific styles if needed */
</style>

View File

@@ -1,242 +0,0 @@
/** * EntitySelectionStep.vue - Entity selection step component * * Extracted
from GiftedDialog.vue to handle the complete step 1 * entity selection interface
with dynamic labeling and grid display. * * @author Matthew Raymer */
<template>
<div id="sectionGiftedGiver">
<label class="block font-bold mb-4">
{{ stepLabel }}
</label>
<EntityGrid
:entity-type="shouldShowProjects ? 'projects' : 'people'"
:entities="shouldShowProjects ? projects : allContacts"
:max-items="10"
:active-did="activeDid"
:all-my-dids="allMyDids"
:all-contacts="allContacts"
:conflict-checker="conflictChecker"
:show-you-entity="shouldShowYouEntity"
:you-selectable="youSelectable"
:show-all-route="showAllRoute"
:show-all-query-params="showAllQueryParams"
@entity-selected="handleEntitySelected"
/>
<button
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-lg"
@click="handleCancel"
>
Cancel
</button>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Emit } from "vue-facing-decorator";
import EntityGrid from "./EntityGrid.vue";
import { Contact } from "../db/tables/contacts";
import { PlanData } from "../interfaces/records";
/**
* Entity selection event data structure
*/
interface EntitySelectionEvent {
type: "person" | "project" | "special";
entityType?: string;
data: any;
}
/**
* EntitySelectionStep - Complete step 1 entity selection interface
*
* Features:
* - Dynamic step labeling based on context
* - EntityGrid integration for unified entity display
* - Conflict detection and prevention
* - Special entity handling (You, Unnamed)
* - Show All navigation with context preservation
* - Cancel functionality
* - Event delegation for entity selection
*/
@Component({
components: {
EntityGrid,
},
})
export default class EntitySelectionStep extends Vue {
/** Type of step: 'giver' or 'recipient' */
@Prop({ required: true })
stepType!: "giver" | "recipient";
/** Type of giver entity: 'person' or 'project' */
@Prop({ required: true })
giverEntityType!: "person" | "project";
/** Type of recipient entity: 'person' or 'project' */
@Prop({ required: true })
recipientEntityType!: "person" | "project";
/** Whether to show projects instead of people */
@Prop({ default: false })
showProjects!: boolean;
/** Whether this is from a project view */
@Prop({ default: false })
isFromProjectView!: boolean;
/** Array of available projects */
@Prop({ required: true })
projects!: PlanData[];
/** Array of available contacts */
@Prop({ required: true })
allContacts!: Contact[];
/** Active user's DID */
@Prop({ required: true })
activeDid!: string;
/** All user's DIDs */
@Prop({ required: true })
allMyDids!: string[];
/** Function to check if a DID would create a conflict */
@Prop({ required: true })
conflictChecker!: (did: string) => boolean;
/** Project ID for context (giver) */
@Prop({ default: "" })
fromProjectId!: string;
/** Project ID for context (recipient) */
@Prop({ default: "" })
toProjectId!: string;
/** Current giver entity for context */
@Prop()
giver?: any;
/** Current receiver entity for context */
@Prop()
receiver?: any;
/**
* Computed step label based on context
*/
get stepLabel(): string {
if (this.stepType === "recipient") {
return "Choose who received the gift:";
} else if (this.showProjects) {
return "Choose a project benefitted from:";
} else {
return "Choose a person received from:";
}
}
/**
* Whether to show projects in the grid
*/
get shouldShowProjects(): boolean {
return (
(this.stepType === "giver" && this.giverEntityType === "project") ||
(this.stepType === "recipient" && this.recipientEntityType === "project")
);
}
/**
* Whether to show the "You" entity
*/
get shouldShowYouEntity(): boolean {
return (
this.stepType === "recipient" ||
(this.stepType === "giver" && this.isFromProjectView)
);
}
/**
* Whether the "You" entity is selectable
*/
get youSelectable(): boolean {
return !this.conflictChecker(this.activeDid);
}
/**
* Route name for "Show All" navigation
*/
get showAllRoute(): string {
if (this.shouldShowProjects) {
return "discover";
} else if (this.allContacts.length > 0) {
return "contact-gift";
}
return "";
}
/**
* Query parameters for "Show All" navigation
*/
get showAllQueryParams(): Record<string, any> {
if (this.shouldShowProjects) {
return {};
}
return {
stepType: this.stepType,
giverEntityType: this.giverEntityType,
recipientEntityType: this.recipientEntityType,
...(this.stepType === "giver"
? {
recipientProjectId: this.toProjectId,
recipientProjectName: this.receiver?.name,
recipientProjectImage: this.receiver?.image,
recipientProjectHandleId: this.receiver?.handleId,
recipientDid: this.receiver?.did,
}
: {
giverProjectId: this.fromProjectId,
giverProjectName: this.giver?.name,
giverProjectImage: this.giver?.image,
giverProjectHandleId: this.giver?.handleId,
giverDid: this.giver?.did,
}),
fromProjectId: this.fromProjectId,
toProjectId: this.toProjectId,
showProjects: this.showProjects.toString(),
isFromProjectView: this.isFromProjectView.toString(),
};
}
/**
* Handle entity selection from EntityGrid
*/
handleEntitySelected(event: EntitySelectionEvent): void {
this.emitEntitySelected({
stepType: this.stepType,
...event,
});
}
/**
* Handle cancel button click
*/
handleCancel(): void {
this.emitCancel();
}
// Emit methods using @Emit decorator
@Emit("entity-selected")
emitEntitySelected(data: any): any {
return data;
}
@Emit("cancel")
emitCancel(): void {
// No return value needed
}
}
</script>
<style scoped>
/* Component-specific styles if needed */
</style>

View File

@@ -1,145 +0,0 @@
/** * EntitySummaryButton.vue - Displays selected entity with edit capability *
* Extracted from GiftedDialog.vue to handle entity summary display * in the gift
details step with edit functionality. * * @author Matthew Raymer */
<template>
<component
:is="editable ? 'button' : 'div'"
class="flex-1 flex items-center gap-2 bg-slate-100 border border-slate-300 rounded-md p-2"
@click="handleClick"
>
<!-- Entity Icon/Avatar -->
<div>
<template v-if="entityType === 'project'">
<ProjectIcon
v-if="entity?.handleId"
:entity-id="entity.handleId"
:icon-size="32"
:image-url="entity.image"
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
/>
</template>
<template v-else>
<EntityIcon
v-if="entity?.did"
:contact="entity"
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
/>
<font-awesome
v-else
icon="circle-question"
class="text-slate-400 text-3xl"
/>
</template>
</div>
<!-- Entity Information -->
<div class="text-start min-w-0">
<p class="text-xs text-slate-500 leading-1 -mb-1 uppercase">
{{ label }}
</p>
<h3 class="font-semibold truncate">
{{ entity?.name || "Unnamed" }}
</h3>
</div>
<!-- Edit/Lock Icon -->
<p class="ms-auto text-sm pe-1" :class="iconClasses">
<font-awesome
:icon="editable ? 'pen' : 'lock'"
:title="editable ? 'Change' : 'Can\'t be changed'"
/>
</p>
</component>
</template>
<script lang="ts">
import { Component, Prop, Vue, Emit } from "vue-facing-decorator";
import EntityIcon from "./EntityIcon.vue";
import ProjectIcon from "./ProjectIcon.vue";
import { Contact } from "../db/tables/contacts";
/**
* Entity interface for both person and project entities
*/
interface EntityData {
did?: string;
handleId?: string;
name?: string;
image?: string;
}
/**
* EntitySummaryButton - Displays selected entity with optional edit capability
*
* Features:
* - Shows entity avatar (person or project)
* - Displays entity name and role label
* - Handles editable vs locked states
* - Emits edit events when clicked and editable
* - Supports both person and project entity types
*/
@Component({
components: {
EntityIcon,
ProjectIcon,
},
})
export default class EntitySummaryButton extends Vue {
/** Entity data to display */
@Prop({ required: true })
entity!: EntityData | Contact | null;
/** Type of entity: 'person' or 'project' */
@Prop({ required: true })
entityType!: "person" | "project";
/** Display label for the entity role */
@Prop({ required: true })
label!: string;
/** Whether the entity can be edited */
@Prop({ default: true })
editable!: boolean;
/**
* Computed CSS classes for the edit/lock icon
*/
get iconClasses(): string {
return this.editable ? "text-blue-500" : "text-slate-400";
}
/**
* Handle click event - only emit if editable
*/
handleClick(): void {
if (this.editable) {
this.emitEditRequested({
entityType: this.entityType,
entity: this.entity,
});
}
}
// Emit methods using @Emit decorator
@Emit("edit-requested")
emitEditRequested(data: any): any {
return data;
}
}
</script>
<style scoped>
/* Ensure button styling is consistent */
button {
cursor: pointer;
}
button:hover {
background-color: #f1f5f9; /* hover:bg-slate-100 */
}
div {
cursor: default;
}
</style>

View File

@@ -100,11 +100,6 @@ import {
} from "@vue-leaflet/vue-leaflet"; } from "@vue-leaflet/vue-leaflet";
import { Router } from "vue-router"; import { Router } from "vue-router";
import { USE_DEXIE_DB } from "@/constants/app";
import * as databaseUtil from "../db/databaseUtil";
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
@Component({ @Component({
components: { components: {
LRectangle, LRectangle,
@@ -125,10 +120,8 @@ export default class FeedFilters extends Vue {
async open(onCloseIfChanged: () => void) { async open(onCloseIfChanged: () => void) {
this.onCloseIfChanged = onCloseIfChanged; this.onCloseIfChanged = onCloseIfChanged;
let settings = await databaseUtil.retrieveSettingsForActiveAccount(); const platform = this.$platform;
if (USE_DEXIE_DB) { const settings = await platform.getActiveAccountSettings();
settings = await retrieveSettingsForActiveAccount();
}
this.hasVisibleDid = !!settings.filterFeedByVisible; this.hasVisibleDid = !!settings.filterFeedByVisible;
this.isNearby = !!settings.filterFeedByNearby; this.isNearby = !!settings.filterFeedByNearby;
if (settings.searchBoxes && settings.searchBoxes.length > 0) { if (settings.searchBoxes && settings.searchBoxes.length > 0) {
@@ -142,29 +135,19 @@ export default class FeedFilters extends Vue {
async toggleHasVisibleDid() { async toggleHasVisibleDid() {
this.settingChanged = true; this.settingChanged = true;
this.hasVisibleDid = !this.hasVisibleDid; this.hasVisibleDid = !this.hasVisibleDid;
await databaseUtil.updateDefaultSettings({ const platform = this.$platform;
await platform.updateMasterSettings({
filterFeedByVisible: this.hasVisibleDid, filterFeedByVisible: this.hasVisibleDid,
}); });
if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, {
filterFeedByVisible: this.hasVisibleDid,
});
}
} }
async toggleNearby() { async toggleNearby() {
this.settingChanged = true; this.settingChanged = true;
this.isNearby = !this.isNearby; this.isNearby = !this.isNearby;
await databaseUtil.updateDefaultSettings({ const platform = this.$platform;
await platform.updateMasterSettings({
filterFeedByNearby: this.isNearby, filterFeedByNearby: this.isNearby,
}); });
if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, {
filterFeedByNearby: this.isNearby,
});
}
} }
async clearAll() { async clearAll() {
@@ -172,18 +155,12 @@ export default class FeedFilters extends Vue {
this.settingChanged = true; this.settingChanged = true;
} }
await databaseUtil.updateDefaultSettings({ const platform = this.$platform;
await platform.updateMasterSettings({
filterFeedByNearby: false, filterFeedByNearby: false,
filterFeedByVisible: false, filterFeedByVisible: false,
}); });
if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, {
filterFeedByNearby: false,
filterFeedByVisible: false,
});
}
this.hasVisibleDid = false; this.hasVisibleDid = false;
this.isNearby = false; this.isNearby = false;
} }
@@ -193,18 +170,12 @@ export default class FeedFilters extends Vue {
this.settingChanged = true; this.settingChanged = true;
} }
await databaseUtil.updateDefaultSettings({ const platform = this.$platform;
await platform.updateMasterSettings({
filterFeedByNearby: true, filterFeedByNearby: true,
filterFeedByVisible: true, filterFeedByVisible: true,
}); });
if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, {
filterFeedByNearby: true,
filterFeedByVisible: true,
});
}
this.hasVisibleDid = true; this.hasVisibleDid = true;
this.isNearby = true; this.isNearby = true;
} }

View File

@@ -1,414 +0,0 @@
/** * GiftDetailsStep.vue - Gift details step component * * Extracted from
GiftedDialog.vue to handle the complete step 2 * gift details form interface
with entity summaries and validation. * * @author Matthew Raymer */
<template>
<div id="sectionGiftedGift">
<!-- Entity Summary Buttons -->
<div class="grid grid-cols-2 gap-2 mb-4">
<!-- Giver Button -->
<EntitySummaryButton
:entity="giver"
:entity-type="giverEntityType"
:label="giverLabel"
:editable="canEditGiver"
@edit-requested="handleEditGiver"
/>
<!-- Recipient Button -->
<EntitySummaryButton
:entity="receiver"
:entity-type="recipientEntityType"
:label="recipientLabel"
:editable="canEditRecipient"
@edit-requested="handleEditRecipient"
/>
</div>
<!-- Gift Description Input -->
<input
v-model="localDescription"
type="text"
class="block w-full rounded border border-slate-400 px-3 py-2 mb-4 placeholder:italic"
:placeholder="prompt || 'What was given?'"
@input="handleDescriptionChange"
/>
<!-- Amount Input and Unit Selection -->
<div class="flex mb-4">
<AmountInput
:value="localAmount"
:min="0"
input-id="inputGivenAmount"
@update:value="handleAmountChange"
/>
<select
v-model="localUnitCode"
class="flex-1 rounded border border-slate-400 ms-2 px-3 py-2"
@change="handleUnitCodeChange"
>
<option value="HUR">Hours</option>
<option value="USD">US $</option>
<option value="BTC">BTC</option>
<option value="BX">BX</option>
<option value="ETH">ETH</option>
</select>
</div>
<!-- Photo & More Options Link -->
<router-link
:to="photoOptionsRoute"
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-lg mb-4"
>
Photo &amp; more options&hellip;
</router-link>
<!-- Sign & Send Info -->
<p class="text-center text-sm mb-4">
<b class="font-medium">Sign &amp; Send</b> to publish to the world
<font-awesome
icon="circle-info"
class="fa-fw text-blue-500 text-base cursor-pointer"
@click="handleExplainData"
/>
</p>
<!-- Conflict Warning -->
<div
v-if="hasConflict"
class="mb-4 p-3 bg-red-50 border border-red-200 rounded-md"
>
<p class="text-red-700 text-sm text-center">
<font-awesome icon="exclamation-triangle" class="fa-fw mr-1" />
Cannot record: Same person selected as both giver and recipient
</p>
</div>
<!-- Action Buttons -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<button
:disabled="hasConflict"
:class="submitButtonClasses"
@click="handleSubmit"
>
Sign &amp; Send
</button>
<button
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-lg"
@click="handleCancel"
>
Cancel
</button>
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch, Emit } from "vue-facing-decorator";
import EntitySummaryButton from "./EntitySummaryButton.vue";
import AmountInput from "./AmountInput.vue";
import { RouteLocationRaw } from "vue-router";
/**
* Entity data interface for giver/receiver
*/
interface EntityData {
did?: string;
handleId?: string;
name?: string;
image?: string;
}
/**
* GiftDetailsStep - Complete step 2 gift details form interface
*
* Features:
* - Entity summary display with edit capability
* - Gift description input with placeholder support
* - Amount input with increment/decrement controls
* - Unit code selection (HUR, USD, BTC, etc.)
* - Photo & more options navigation
* - Conflict detection and warning display
* - Form validation and submission
* - Cancel functionality
*/
@Component({
components: {
EntitySummaryButton,
AmountInput,
},
})
export default class GiftDetailsStep extends Vue {
/** Giver entity data */
@Prop({ required: true })
giver!: EntityData | null;
/** Receiver entity data */
@Prop({ required: true })
receiver!: EntityData | null;
/** Type of giver entity: 'person' or 'project' */
@Prop({ required: true })
giverEntityType!: "person" | "project";
/** Type of recipient entity: 'person' or 'project' */
@Prop({ required: true })
recipientEntityType!: "person" | "project";
/** Gift description */
@Prop({ default: "" })
description!: string;
/** Gift amount */
@Prop({ default: 0 })
amount!: number;
/** Unit code (HUR, USD, etc.) */
@Prop({ default: "HUR" })
unitCode!: string;
/** Input placeholder text */
@Prop({ default: "" })
prompt!: string;
/** Whether this is from a project view */
@Prop({ default: false })
isFromProjectView!: boolean;
/** Whether there's a conflict between giver and receiver */
@Prop({ default: false })
hasConflict!: boolean;
/** Offer ID for context */
@Prop({ default: "" })
offerId!: string;
/** Project ID for context (giver) */
@Prop({ default: "" })
fromProjectId!: string;
/** Project ID for context (recipient) */
@Prop({ default: "" })
toProjectId!: string;
/** Local reactive copies of props for v-model */
private localDescription: string = "";
private localAmount: number = 0;
private localUnitCode: string = "HUR";
/**
* Initialize local values from props
*/
mounted(): void {
this.localDescription = this.description;
this.localAmount = this.amount;
this.localUnitCode = this.unitCode;
}
/**
* Watch for external prop changes
*/
@Watch("description")
onDescriptionChange(newValue: string): void {
this.localDescription = newValue;
}
@Watch("amount")
onAmountChange(newValue: number): void {
this.localAmount = newValue;
}
@Watch("unitCode")
onUnitCodeChange(newValue: string): void {
this.localUnitCode = newValue;
}
/**
* Computed label for giver entity
*/
get giverLabel(): string {
return this.giverEntityType === "project"
? "Benefited from:"
: "Received from:";
}
/**
* Computed label for recipient entity
*/
get recipientLabel(): string {
return this.recipientEntityType === "project"
? "Given to project:"
: "Given to:";
}
/**
* Whether the giver can be edited
*/
get canEditGiver(): boolean {
return !(this.isFromProjectView && this.giverEntityType === "project");
}
/**
* Whether the recipient can be edited
*/
get canEditRecipient(): boolean {
return this.recipientEntityType === "person";
}
/**
* Computed CSS classes for submit button
*/
get submitButtonClasses(): string {
if (this.hasConflict) {
return "block w-full text-center text-md uppercase font-bold bg-gradient-to-b from-slate-300 to-slate-500 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-slate-400 px-1.5 py-2 rounded-lg cursor-not-allowed";
}
return "block w-full text-center text-md uppercase font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-lg";
}
/**
* Computed route for photo & more options
*/
get photoOptionsRoute(): RouteLocationRaw {
return {
name: "gifted-details",
query: {
amountInput: this.localAmount.toString(),
description: this.localDescription,
giverDid:
this.giverEntityType === "person" ? this.giver?.did : undefined,
giverName: this.giver?.name,
offerId: this.offerId,
fulfillsProjectId:
this.giverEntityType === "person" &&
this.recipientEntityType === "project"
? this.toProjectId
: undefined,
providerProjectId:
this.giverEntityType === "project" &&
this.recipientEntityType === "person"
? this.giver?.handleId
: this.fromProjectId,
recipientDid: this.receiver?.did,
recipientName: this.receiver?.name,
unitCode: this.localUnitCode,
},
};
}
/**
* Handle description input changes
*/
handleDescriptionChange(): void {
this.emitUpdateDescription(this.localDescription);
}
/**
* Handle amount input changes
*/
handleAmountChange(newAmount: number): void {
console.log(
`[GiftDetailsStep] handleAmountChange() called - oldAmount: ${this.localAmount}, newAmount: ${newAmount}`,
);
this.localAmount = newAmount;
this.emitUpdateAmount(newAmount);
}
/**
* Handle unit code selection changes
*/
handleUnitCodeChange(): void {
this.emitUpdateUnitCode(this.localUnitCode);
}
/**
* Handle giver edit request
*/
handleEditGiver(): void {
this.emitEditEntity({
entityType: "giver",
currentEntity: this.giver,
});
}
/**
* Handle recipient edit request
*/
handleEditRecipient(): void {
this.emitEditEntity({
entityType: "recipient",
currentEntity: this.receiver,
});
}
/**
* Handle explain data info click
*/
handleExplainData(): void {
this.emitExplainData();
}
/**
* Handle form submission
*/
handleSubmit(): void {
if (!this.hasConflict) {
this.emitSubmit({
description: this.localDescription,
amount: this.localAmount,
unitCode: this.localUnitCode,
});
}
}
/**
* Handle cancel button click
*/
handleCancel(): void {
this.emitCancel();
}
// Emit methods using @Emit decorator
@Emit("update:description")
emitUpdateDescription(description: string): string {
return description;
}
@Emit("update:amount")
emitUpdateAmount(amount: number): number {
console.log(
`[GiftDetailsStep] emitUpdateAmount() - emitting amount: ${amount}`,
);
return amount;
}
@Emit("update:unitCode")
emitUpdateUnitCode(unitCode: string): string {
return unitCode;
}
@Emit("edit-entity")
emitEditEntity(data: any): any {
return data;
}
@Emit("explain-data")
emitExplainData(): void {
// No return value needed
}
@Emit("submit")
emitSubmit(data: any): any {
return data;
}
@Emit("cancel")
emitCancel(): void {
// No return value needed
}
}
</script>
<style scoped>
/* Component-specific styles if needed */
</style>

View File

@@ -1,108 +1,112 @@
<template> <template>
<div v-if="visible" class="dialog-overlay"> <div v-if="visible" class="dialog-overlay">
<div class="dialog"> <div class="dialog">
<!-- Step 1: Entity Selection --> <h1 class="text-xl font-bold text-center mb-4">
<EntitySelectionStep {{ customTitle }}
v-show="currentStep === 1" </h1>
:step-type="stepType" <input
:giver-entity-type="giverEntityType" v-model="description"
:recipient-entity-type="recipientEntityType" type="text"
:show-projects="showProjects" class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
:is-from-project-view="isFromProjectView" :placeholder="prompt || 'What was given?'"
:projects="projects"
:all-contacts="allContacts"
:active-did="activeDid"
:all-my-dids="allMyDids"
:conflict-checker="wouldCreateConflict"
:from-project-id="fromProjectId"
:to-project-id="toProjectId"
:giver="giver"
:receiver="receiver"
@entity-selected="handleEntitySelected"
@cancel="cancel"
/>
<!-- Step 2: Gift Details -->
<GiftDetailsStep
v-show="currentStep === 2"
:giver="giver"
:receiver="receiver"
:giver-entity-type="giverEntityType"
:recipient-entity-type="recipientEntityType"
:description="description"
:amount="parseFloat(amountInput) || 0"
:unit-code="unitCode"
:prompt="prompt"
:is-from-project-view="isFromProjectView"
:has-conflict="hasPersonConflict"
:offer-id="offerId"
:from-project-id="fromProjectId"
:to-project-id="toProjectId"
@update:description="description = $event"
@update:amount="handleAmountUpdate"
@update:unit-code="unitCode = $event"
@edit-entity="handleEditEntity"
@explain-data="explainData"
@submit="handleSubmit"
@cancel="cancel"
/> />
<div class="flex flex-row justify-center">
<span
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center text-blue-500 px-2 py-2 w-20"
@click="changeUnitCode()"
>
{{ libsUtil.UNIT_SHORT[unitCode] || unitCode }}
</span>
<div
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
@click="amountInput === '0' ? null : decrement()"
>
<font-awesome icon="chevron-left" />
</div>
<input
id="inputGivenAmount"
v-model="amountInput"
type="number"
class="border border-r-0 border-slate-400 px-2 py-2 text-center w-20"
/>
<div
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
@click="increment()"
>
<font-awesome icon="chevron-right" />
</div>
</div>
<div class="mt-4 flex justify-center">
<span>
<router-link
:to="{
name: 'gifted-details',
query: {
amountInput,
description,
giverDid: giver?.did,
giverName: giver?.name,
offerId,
fulfillsProjectId: toProjectId,
providerProjectId: fromProjectId,
recipientDid: receiver?.did,
recipientName: receiver?.name,
unitCode,
},
}"
class="text-blue-500"
>
Photo & more options ...
</router-link>
</span>
</div>
<p class="text-center mb-2 mt-6 italic">
Sign & Send to publish to the world
<font-awesome
icon="circle-info"
class="pl-2 text-blue-500 cursor-pointer"
@click="explainData()"
/>
</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<button
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
@click="confirm"
>
Sign &amp; Send
</button>
<button
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
@click="cancel"
>
Cancel
</button>
</div>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Vue, Component, Prop, Watch } from "vue-facing-decorator"; import { Vue, Component, Prop } from "vue-facing-decorator";
import { NotificationIface, USE_DEXIE_DB } from "../constants/app"; import { NotificationIface } from "../constants/app";
import { import {
createAndSubmitGive, createAndSubmitGive,
didInfo, didInfo,
serverMessageForUser, serverMessageForUser,
getHeaders,
} from "../libs/endorserServer"; } from "../libs/endorserServer";
import * as libsUtil from "../libs/util"; import * as libsUtil from "../libs/util";
import { db, retrieveSettingsForActiveAccount } from "../db/index"; import { db, retrieveSettingsForActiveAccount } from "../db/index";
import { Contact } from "../db/tables/contacts"; import { Contact } from "../db/tables/contacts";
import * as databaseUtil from "../db/databaseUtil";
import { retrieveAccountDids } from "../libs/util"; import { retrieveAccountDids } from "../libs/util";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import EntityIcon from "../components/EntityIcon.vue";
import ProjectIcon from "../components/ProjectIcon.vue";
import EntitySelectionStep from "../components/EntitySelectionStep.vue";
import GiftDetailsStep from "../components/GiftDetailsStep.vue";
import { PlanData } from "../interfaces/records";
@Component({ @Component
components: {
EntityIcon,
ProjectIcon,
EntitySelectionStep,
GiftDetailsStep,
},
})
export default class GiftedDialog extends Vue { export default class GiftedDialog extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
@Prop() fromProjectId = ""; @Prop() fromProjectId = "";
@Prop() toProjectId = ""; @Prop() toProjectId = "";
@Prop({ default: false }) showProjects = false;
@Prop() isFromProjectView = false;
@Watch("showProjects")
onShowProjectsChange() {
this.updateEntityTypes();
}
@Watch("fromProjectId")
onFromProjectIdChange() {
this.updateEntityTypes();
}
@Watch("toProjectId")
onToProjectIdChange() {
this.updateEntityTypes();
}
activeDid = ""; activeDid = "";
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];
@@ -119,94 +123,9 @@ export default class GiftedDialog extends Vue {
receiver?: libsUtil.GiverReceiverInputInfo; receiver?: libsUtil.GiverReceiverInputInfo;
unitCode = "HUR"; unitCode = "HUR";
visible = false; visible = false;
currentStep = 1;
libsUtil = libsUtil; libsUtil = libsUtil;
projects: PlanData[] = [];
didInfo = didInfo;
// Computed property to help debug template logic
get shouldShowProjects() {
const result =
(this.stepType === "giver" && this.giverEntityType === "project") ||
(this.stepType === "recipient" && this.recipientEntityType === "project");
return result;
}
// Computed property to check if current selection would create a conflict
get hasPersonConflict() {
// Only check for conflicts when both entities are persons
if (
this.giverEntityType !== "person" ||
this.recipientEntityType !== "person"
) {
return false;
}
// Check if giver and recipient are the same person
if (
this.giver?.did &&
this.receiver?.did &&
this.giver.did === this.receiver.did
) {
return true;
}
return false;
}
// Computed property to check if a contact would create a conflict when selected
wouldCreateConflict(contactDid: string) {
// Only check for conflicts when both entities are persons
if (
this.giverEntityType !== "person" ||
this.recipientEntityType !== "person"
) {
return false;
}
if (this.stepType === "giver") {
// If selecting as giver, check if it conflicts with current recipient
return this.receiver?.did === contactDid;
} else if (this.stepType === "recipient") {
// If selecting as recipient, check if it conflicts with current giver
return this.giver?.did === contactDid;
}
return false;
}
stepType = "giver";
giverEntityType = "person" as "person" | "project";
recipientEntityType = "person" as "person" | "project";
updateEntityTypes() {
// Reset and set entity types based on current context
this.giverEntityType = "person";
this.recipientEntityType = "person";
// Determine entity types based on current context
if (this.showProjects) {
// HomeView "Project" button or ProjectViewView "Given by This"
this.giverEntityType = "project";
this.recipientEntityType = "person";
} else if (this.fromProjectId) {
// ProjectViewView "Given by This" button (project is giver)
this.giverEntityType = "project";
this.recipientEntityType = "person";
} else if (this.toProjectId) {
// ProjectViewView "Given to This" button (project is recipient)
this.giverEntityType = "person";
this.recipientEntityType = "project";
} else {
// HomeView "Person" button
this.giverEntityType = "person";
this.recipientEntityType = "person";
}
}
async open( async open(
giver?: libsUtil.GiverReceiverInputInfo, giver?: libsUtil.GiverReceiverInputInfo,
receiver?: libsUtil.GiverReceiverInputInfo, receiver?: libsUtil.GiverReceiverInputInfo,
@@ -219,33 +138,17 @@ export default class GiftedDialog extends Vue {
this.giver = giver; this.giver = giver;
this.prompt = prompt || ""; this.prompt = prompt || "";
this.receiver = receiver; this.receiver = receiver;
// if we show "given to user" selection, default checkbox to true
this.amountInput = "0"; this.amountInput = "0";
this.callbackOnSuccess = callbackOnSuccess; this.callbackOnSuccess = callbackOnSuccess;
this.offerId = offerId || ""; this.offerId = offerId || "";
this.currentStep = giver ? 2 : 1;
this.stepType = "giver";
// Update entity types based on current props
this.updateEntityTypes();
try { try {
let settings = await databaseUtil.retrieveSettingsForActiveAccount(); const settings = await retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.apiServer = settings.apiServer || ""; this.apiServer = settings.apiServer || "";
this.activeDid = settings.activeDid || ""; this.activeDid = settings.activeDid || "";
const platformService = PlatformServiceFactory.getInstance(); this.allContacts = await db.contacts.toArray();
const result = await platformService.dbQuery(`SELECT * FROM contacts`);
if (result) {
this.allContacts = databaseUtil.mapQueryResultToValues(
result,
) as unknown as Contact[];
}
if (USE_DEXIE_DB) {
this.allContacts = await db.contacts.toArray();
}
this.allMyDids = await retrieveAccountDids(); this.allMyDids = await retrieveAccountDids();
@@ -257,16 +160,7 @@ export default class GiftedDialog extends Vue {
this.allContacts, this.allContacts,
); );
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (
this.giverEntityType === "project" ||
this.recipientEntityType === "project"
) {
await this.loadProjects();
} else {
// Clear projects array when not needed
this.projects = [];
}
} catch (err: any) { } catch (err: any) {
logger.error("Error retrieving settings from database:", err); logger.error("Error retrieving settings from database:", err);
this.$notify( this.$notify(
@@ -316,7 +210,6 @@ export default class GiftedDialog extends Vue {
this.amountInput = "0"; this.amountInput = "0";
this.prompt = ""; this.prompt = "";
this.unitCode = "HUR"; this.unitCode = "HUR";
this.currentStep = 1;
} }
async confirm() { async confirm() {
@@ -359,20 +252,6 @@ export default class GiftedDialog extends Vue {
return; return;
} }
// Check for person conflict
if (this.hasPersonConflict) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "You cannot select the same person as both giver and recipient.",
},
3000,
);
return;
}
this.close(); this.close();
this.$notify( this.$notify(
{ {
@@ -411,55 +290,26 @@ export default class GiftedDialog extends Vue {
unitCode: string = "HUR", unitCode: string = "HUR",
) { ) {
try { try {
// Determine the correct parameters based on entity types
let fromDid: string | undefined;
let toDid: string | undefined;
let fulfillsProjectHandleId: string | undefined;
let providerPlanHandleId: string | undefined;
if (
this.giverEntityType === "project" &&
this.recipientEntityType === "person"
) {
// Project-to-person gift
fromDid = undefined; // No person giver
toDid = recipientDid as string; // Person recipient
fulfillsProjectHandleId = undefined; // No project recipient
providerPlanHandleId = this.giver?.handleId; // Project giver
} else if (
this.giverEntityType === "person" &&
this.recipientEntityType === "project"
) {
// Person-to-project gift
fromDid = giverDid as string; // Person giver
toDid = undefined; // No person recipient
fulfillsProjectHandleId = this.toProjectId; // Project recipient
providerPlanHandleId = undefined; // No project giver
} else {
// Person-to-person gift
fromDid = giverDid as string;
toDid = recipientDid as string;
fulfillsProjectHandleId = undefined;
providerPlanHandleId = undefined;
}
const result = await createAndSubmitGive( const result = await createAndSubmitGive(
this.axios, this.axios,
this.apiServer, this.apiServer,
this.activeDid, this.activeDid,
fromDid, giverDid as string,
toDid, recipientDid as string,
description, description,
amount, amount,
unitCode, unitCode,
fulfillsProjectHandleId, this.toProjectId,
this.offerId, this.offerId,
false, false,
undefined, undefined,
providerPlanHandleId, this.fromProjectId,
); );
if (!result.success) { if (
result.type === "error" ||
this.isGiveCreationError(result.response)
) {
const errorMessage = this.getGiveCreationErrorMessage(result); const errorMessage = this.getGiveCreationErrorMessage(result);
logger.error("Error with give creation result:", result); logger.error("Error with give creation result:", result);
this.$notify( this.$notify(
@@ -506,6 +356,15 @@ export default class GiftedDialog extends Vue {
// Helper functions for readability // Helper functions for readability
/**
* @param result response "data" from the server
* @returns true if the result indicates an error
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
isGiveCreationError(result: any) {
return result.status !== 201 || result.data?.error;
}
/** /**
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data") * @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
* @returns best guess at an error message * @returns best guess at an error message
@@ -530,173 +389,6 @@ export default class GiftedDialog extends Vue {
-1, -1,
); );
} }
selectGiver(contact?: Contact) {
if (contact) {
this.giver = {
did: contact.did,
name: contact.name || contact.did,
};
} else {
this.giver = {
did: "",
name: "Unnamed",
};
}
this.currentStep = 2;
}
goBackToStep1(step: string) {
this.stepType = step;
this.currentStep = 1;
}
async loadProjects() {
try {
const response = await fetch(this.apiServer + "/api/v2/report/plans", {
method: "GET",
headers: await getHeaders(this.activeDid),
});
if (response.status !== 200) {
throw new Error("Failed to load projects");
}
const results = await response.json();
if (results.data) {
this.projects = results.data;
}
} catch (error) {
logger.error("Error loading projects:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Failed to load projects",
},
3000,
);
}
}
selectProject(project: PlanData) {
this.giver = {
did: project.handleId,
name: project.name,
image: project.image,
handleId: project.handleId,
};
this.receiver = {
did: this.activeDid,
name: "You",
};
this.currentStep = 2;
}
selectRecipient(contact?: Contact) {
if (contact) {
this.receiver = {
did: contact.did,
name: contact.name || contact.did,
};
} else {
this.receiver = {
did: "",
name: "Unnamed",
};
}
this.currentStep = 2;
}
selectRecipientProject(project: PlanData) {
this.receiver = {
did: project.handleId,
name: project.name,
image: project.image,
handleId: project.handleId,
};
this.currentStep = 2;
}
// Computed property for the query parameters
get giftedDetailsQuery() {
return {
amountInput: this.amountInput,
description: this.description,
giverDid: this.giverEntityType === "person" ? this.giver?.did : undefined,
giverName: this.giver?.name,
offerId: this.offerId,
fulfillsProjectId:
this.giverEntityType === "person" &&
this.recipientEntityType === "project"
? this.toProjectId
: undefined,
providerProjectId:
this.giverEntityType === "project" &&
this.recipientEntityType === "person"
? this.giver?.handleId
: this.fromProjectId,
recipientDid: this.receiver?.did,
recipientName: this.receiver?.name,
unitCode: this.unitCode,
};
}
// New event handlers for component integration
/**
* Handle entity selection from EntitySelectionStep
* @param entity - The selected entity (person or project)
*/
handleEntitySelected(entity: {
type: "person" | "project";
data: Contact | PlanData;
}) {
if (entity.type === "person") {
const contact = entity.data as Contact;
if (this.stepType === "giver") {
this.selectGiver(contact);
} else {
this.selectRecipient(contact);
}
} else {
const project = entity.data as PlanData;
if (this.stepType === "giver") {
this.selectProject(project);
} else {
this.selectRecipientProject(project);
}
}
}
/**
* Handle edit entity request from GiftDetailsStep
* @param entityType - 'giver' or 'recipient'
*/
handleEditEntity(entityType: "giver" | "recipient") {
this.goBackToStep1(entityType);
}
/**
* Handle form submission from GiftDetailsStep
*/
handleSubmit() {
this.confirm();
}
/**
* Handle amount update from GiftDetailsStep
*/
handleAmountUpdate(newAmount: number) {
console.log(
`[GiftedDialog] handleAmountUpdate() called - oldAmount: ${this.amountInput}, newAmount: ${newAmount}`,
);
this.amountInput = newAmount.toString();
console.log(
`[GiftedDialog] handleAmountUpdate() - amountInput updated to: ${this.amountInput}`,
);
}
} }
</script> </script>

View File

@@ -74,12 +74,10 @@
import { Vue, Component } from "vue-facing-decorator"; import { Vue, Component } from "vue-facing-decorator";
import { Router } from "vue-router"; import { Router } from "vue-router";
import { AppString, NotificationIface, USE_DEXIE_DB } from "../constants/app"; import { AppString, NotificationIface } from "../constants/app";
import { db } from "../db/index"; import { db } from "../db/index";
import { Contact } from "../db/tables/contacts"; import { Contact } from "../db/tables/contacts";
import * as databaseUtil from "../db/databaseUtil";
import { GiverReceiverInputInfo } from "../libs/util"; import { GiverReceiverInputInfo } from "../libs/util";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
@Component @Component
export default class GivenPrompts extends Vue { export default class GivenPrompts extends Vue {
@@ -129,16 +127,8 @@ export default class GivenPrompts extends Vue {
this.visible = true; this.visible = true;
this.callbackOnFullGiftInfo = callbackOnFullGiftInfo; this.callbackOnFullGiftInfo = callbackOnFullGiftInfo;
const platformService = PlatformServiceFactory.getInstance(); await db.open();
const result = await platformService.dbQuery( this.numContacts = await db.contacts.count();
"SELECT COUNT(*) FROM contacts",
);
if (result) {
this.numContacts = result.values[0][0] as number;
}
if (USE_DEXIE_DB) {
this.numContacts = await db.contacts.count();
}
this.shownContactDbIndices = new Array<boolean>(this.numContacts); // all undefined to start this.shownContactDbIndices = new Array<boolean>(this.numContacts); // all undefined to start
} }
@@ -227,7 +217,6 @@ export default class GivenPrompts extends Vue {
let someContactDbIndex = Math.floor(Math.random() * this.numContacts); let someContactDbIndex = Math.floor(Math.random() * this.numContacts);
let count = 0; let count = 0;
// as long as the index has an entry, loop // as long as the index has an entry, loop
while ( while (
this.shownContactDbIndices[someContactDbIndex] != null && this.shownContactDbIndices[someContactDbIndex] != null &&
@@ -240,21 +229,10 @@ export default class GivenPrompts extends Vue {
this.nextIdeaPastContacts(); this.nextIdeaPastContacts();
} else { } else {
// get the contact at that offset // get the contact at that offset
const platformService = PlatformServiceFactory.getInstance(); await db.open();
const result = await platformService.dbQuery( this.currentContact = await db.contacts
"SELECT * FROM contacts LIMIT 1 OFFSET ?", .offset(someContactDbIndex)
[someContactDbIndex], .first();
);
if (result) {
const mappedContacts = databaseUtil.mapQueryResultToValues(result);
this.currentContact = mappedContacts[0] as unknown as Contact;
}
if (USE_DEXIE_DB) {
await db.open();
this.currentContact = await db.contacts
.offset(someContactDbIndex)
.first();
}
this.shownContactDbIndices[someContactDbIndex] = true; this.shownContactDbIndices[someContactDbIndex] = true;
} }
} }

View File

@@ -48,7 +48,11 @@
<span> <span>
{{ didInfo(visDid) }} {{ didInfo(visDid) }}
<span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)"> <span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)">
<a :href="`/did/${visDid}`" class="text-blue-500"> <a
:href="`/did/${visDid}`"
target="_blank"
class="text-blue-500"
>
<font-awesome <font-awesome
icon="arrow-up-right-from-square" icon="arrow-up-right-from-square"
class="fa-fw" class="fa-fw"

View File

@@ -4,9 +4,7 @@
<div class="text-lg text-center font-bold relative"> <div class="text-lg text-center font-bold relative">
<h1 id="ViewHeading" class="text-center font-bold"> <h1 id="ViewHeading" class="text-center font-bold">
<span v-if="uploading">Uploading Image&hellip;</span> <span v-if="uploading">Uploading Image&hellip;</span>
<span v-else-if="blob">{{ <span v-else-if="blob">Crop Image</span>
crop ? "Crop Image" : "Preview Image"
}}</span>
<span v-else-if="showCameraPreview">Upload Image</span> <span v-else-if="showCameraPreview">Upload Image</span>
<span v-else>Add Photo</span> <span v-else>Add Photo</span>
</h1> </h1>
@@ -121,23 +119,12 @@
playsinline playsinline
muted muted
></video> ></video>
<div <button
class="absolute bottom-4 inset-x-0 flex items-center justify-center gap-4" class="absolute bottom-4 left-1/2 -translate-x-1/2 bg-white text-slate-800 p-3 rounded-full text-2xl leading-none"
@click="capturePhoto"
> >
<button <font-awesome icon="camera" class="w-[1em]" />
class="bg-white text-slate-800 p-3 rounded-full text-2xl leading-none" </button>
@click="capturePhoto"
>
<font-awesome icon="camera" class="w-[1em]" />
</button>
<button
v-if="platformCapabilities.isMobile"
class="bg-white text-slate-800 p-3 rounded-full text-2xl leading-none"
@click="rotateCamera"
>
<font-awesome icon="rotate" class="w-[1em]" />
</button>
</div>
</div> </div>
</div> </div>
<div <div
@@ -242,12 +229,12 @@
<p class="mb-2"> <p class="mb-2">
Before you can upload a photo, a friend needs to register you. Before you can upload a photo, a friend needs to register you.
</p> </p>
<button <router-link
:to="{ name: 'contact-qr' }"
class="inline-block text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md" class="inline-block text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
@click="handleQRCodeClick"
> >
Share Your Info Share Your Info
</button> </router-link>
</div> </div>
</template> </template>
</div> </div>
@@ -260,17 +247,11 @@ import axios from "axios";
import { ref } from "vue"; import { ref } from "vue";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import VuePictureCropper, { cropper } from "vue-picture-cropper"; import VuePictureCropper, { cropper } from "vue-picture-cropper";
import { Capacitor } from "@capacitor/core"; import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
import {
DEFAULT_IMAGE_API_SERVER,
NotificationIface,
USE_DEXIE_DB,
} from "../constants/app";
import { retrieveSettingsForActiveAccount } from "../db/index"; import { retrieveSettingsForActiveAccount } from "../db/index";
import { accessToken } from "../libs/crypto"; import { accessToken } from "../libs/crypto";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "../services/PlatformServiceFactory"; import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
import * as databaseUtil from "../db/databaseUtil";
const inputImageFileNameRef = ref<Blob>(); const inputImageFileNameRef = ref<Blob>();
@@ -281,11 +262,6 @@ const inputImageFileNameRef = ref<Blob>();
type: Boolean, type: Boolean,
default: true, default: true,
}, },
defaultCameraMode: {
type: String,
default: "environment",
validator: (value: string) => ["environment", "user"].includes(value),
},
}, },
}) })
export default class ImageMethodDialog extends Vue { export default class ImageMethodDialog extends Vue {
@@ -327,9 +303,6 @@ export default class ImageMethodDialog extends Vue {
/** Camera stream reference */ /** Camera stream reference */
private cameraStream: MediaStream | null = null; private cameraStream: MediaStream | null = null;
/** Current camera facing mode */
private currentFacingMode: "environment" | "user" = "environment";
private platformService = PlatformServiceFactory.getInstance(); private platformService = PlatformServiceFactory.getInstance();
URL = window.URL || window.webkitURL; URL = window.URL || window.webkitURL;
@@ -361,10 +334,7 @@ export default class ImageMethodDialog extends Vue {
*/ */
async mounted() { async mounted() {
try { try {
let settings = await databaseUtil.retrieveSettingsForActiveAccount(); const settings = await retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.activeDid = settings.activeDid || ""; this.activeDid = settings.activeDid || "";
} catch (error: unknown) { } catch (error: unknown) {
logger.error("Error retrieving settings from database:", error); logger.error("Error retrieving settings from database:", error);
@@ -391,16 +361,15 @@ export default class ImageMethodDialog extends Vue {
} }
open(setImageFn: (arg: string) => void, claimType: string, crop?: boolean) { open(setImageFn: (arg: string) => void, claimType: string, crop?: boolean) {
logger.debug("ImageMethodDialog.open called");
this.claimType = claimType; this.claimType = claimType;
this.crop = !!crop; this.crop = !!crop;
this.imageCallback = setImageFn; this.imageCallback = setImageFn;
this.visible = true; this.visible = true;
this.currentFacingMode = this.defaultCameraMode as "environment" | "user";
// Start camera preview immediately // Start camera preview immediately if not on mobile
logger.debug("Starting camera preview from open()"); if (!this.platformCapabilities.isNativeApp) {
this.startCameraPreview(); this.startCameraPreview();
}
} }
async uploadImageFile(event: Event) { async uploadImageFile(event: Event) {
@@ -469,24 +438,46 @@ export default class ImageMethodDialog extends Vue {
logger.debug("startCameraPreview called"); logger.debug("startCameraPreview called");
logger.debug("Current showCameraPreview state:", this.showCameraPreview); logger.debug("Current showCameraPreview state:", this.showCameraPreview);
logger.debug("Platform capabilities:", this.platformCapabilities); logger.debug("Platform capabilities:", this.platformCapabilities);
logger.debug("MediaDevices available:", !!navigator.mediaDevices);
logger.debug(
"getUserMedia available:",
!!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia),
);
if (this.platformCapabilities.isNativeApp) {
logger.debug("Using platform service for mobile device");
this.cameraState = "initializing";
this.cameraStateMessage = "Using platform camera service...";
try {
const result = await this.platformService.takePicture();
this.blob = result.blob;
this.fileName = result.fileName;
this.cameraState = "ready";
this.cameraStateMessage = "Photo captured successfully";
} catch (error) {
logger.error("Error taking picture:", error);
this.cameraState = "error";
this.cameraStateMessage =
error instanceof Error ? error.message : "Failed to take picture";
this.error =
error instanceof Error ? error.message : "Failed to take picture";
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Failed to take picture. Please try again.",
},
5000,
);
}
return;
}
logger.debug("Starting camera preview for desktop browser");
try { try {
this.cameraState = "initializing"; this.cameraState = "initializing";
this.cameraStateMessage = "Requesting camera access..."; this.cameraStateMessage = "Requesting camera access...";
this.showCameraPreview = true; this.showCameraPreview = true;
await this.$nextTick(); await this.$nextTick();
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
throw new Error("Camera API not available in this browser");
}
const stream = await navigator.mediaDevices.getUserMedia({ const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: this.currentFacingMode }, video: { facingMode: "environment" },
}); });
logger.debug("Camera access granted"); logger.debug("Camera access granted");
this.cameraStream = stream; this.cameraStream = stream;
@@ -500,36 +491,25 @@ export default class ImageMethodDialog extends Vue {
videoElement.srcObject = stream; videoElement.srcObject = stream;
await new Promise((resolve) => { await new Promise((resolve) => {
videoElement.onloadedmetadata = () => { videoElement.onloadedmetadata = () => {
videoElement videoElement.play().then(() => {
.play() resolve(true);
.then(() => { });
logger.debug("Video element started playing");
resolve(true);
})
.catch((error) => {
logger.error("Error playing video:", error);
throw error;
});
}; };
}); });
} else {
logger.error("Video element not found");
throw new Error("Video element not found");
} }
} catch (error) { } catch (error) {
logger.error("Error starting camera preview:", error); logger.error("Error starting camera preview:", error);
let errorMessage = let errorMessage =
error instanceof Error ? error.message : "Failed to access camera"; error instanceof Error ? error.message : "Failed to access camera";
if ( if (
error instanceof Error && error.name === "NotReadableError" ||
(error.name === "NotReadableError" || error.name === "TrackStartError") error.name === "TrackStartError"
) { ) {
errorMessage = errorMessage =
"Camera is in use by another application. Please close any other apps or browser tabs using the camera and try again."; "Camera is in use by another application. Please close any other apps or browser tabs using the camera and try again.";
} else if ( } else if (
error instanceof Error && error.name === "NotAllowedError" ||
(error.name === "NotAllowedError" || error.name === "PermissionDeniedError"
error.name === "PermissionDeniedError")
) { ) {
errorMessage = errorMessage =
"Camera access was denied. Please allow camera access in your browser settings."; "Camera access was denied. Please allow camera access in your browser settings.";
@@ -537,7 +517,6 @@ export default class ImageMethodDialog extends Vue {
this.cameraState = "error"; this.cameraState = "error";
this.cameraStateMessage = errorMessage; this.cameraStateMessage = errorMessage;
this.error = errorMessage; this.error = errorMessage;
this.showCameraPreview = false;
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -547,6 +526,7 @@ export default class ImageMethodDialog extends Vue {
}, },
5000, 5000,
); );
this.showCameraPreview = false;
} }
} }
@@ -598,21 +578,6 @@ export default class ImageMethodDialog extends Vue {
} }
} }
async rotateCamera() {
// Toggle between front and back cameras
this.currentFacingMode =
this.currentFacingMode === "environment" ? "user" : "environment";
// Stop current stream
if (this.cameraStream) {
this.cameraStream.getTracks().forEach((track) => track.stop());
this.cameraStream = null;
}
// Start new stream with updated facing mode
await this.startCameraPreview();
}
private createBlobURL(blob: Blob): string { private createBlobURL(blob: Blob): string {
return URL.createObjectURL(blob); return URL.createObjectURL(blob);
} }
@@ -647,7 +612,6 @@ export default class ImageMethodDialog extends Vue {
5000, 5000,
); );
this.uploading = false; this.uploading = false;
this.close();
return; return;
} }
formData.append("image", this.blob, this.fileName || "photo.jpg"); formData.append("image", this.blob, this.fileName || "photo.jpg");
@@ -702,7 +666,6 @@ export default class ImageMethodDialog extends Vue {
); );
this.uploading = false; this.uploading = false;
this.blob = undefined; this.blob = undefined;
this.close();
} }
} }
@@ -710,14 +673,6 @@ export default class ImageMethodDialog extends Vue {
toggleDiagnostics() { toggleDiagnostics() {
this.showDiagnostics = !this.showDiagnostics; this.showDiagnostics = !this.showDiagnostics;
} }
private handleQRCodeClick() {
if (Capacitor.isNativePlatform()) {
this.$router.push({ name: "contact-qr-scan-full" });
} else {
this.$router.push({ name: "contact-qr" });
}
}
} }
</script> </script>

View File

@@ -172,10 +172,8 @@ import {
} from "../libs/endorserServer"; } from "../libs/endorserServer";
import { decryptMessage } from "../libs/crypto"; import { decryptMessage } from "../libs/crypto";
import { Contact } from "../db/tables/contacts"; import { Contact } from "../db/tables/contacts";
import * as databaseUtil from "../db/databaseUtil";
import * as libsUtil from "../libs/util"; import * as libsUtil from "../libs/util";
import { NotificationIface, USE_DEXIE_DB } from "../constants/app"; import { NotificationIface } from "../constants/app";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
interface Member { interface Member {
admitted: boolean; admitted: boolean;
@@ -211,10 +209,7 @@ export default class MembersList extends Vue {
contacts: Array<Contact> = []; contacts: Array<Contact> = [];
async created() { async created() {
let settings = await databaseUtil.retrieveSettingsForActiveAccount(); const settings = await retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.activeDid = settings.activeDid || ""; this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || ""; this.apiServer = settings.apiServer || "";
this.firstName = settings.firstName || ""; this.firstName = settings.firstName || "";
@@ -301,7 +296,7 @@ export default class MembersList extends Vue {
this.decryptedMembers.length === 0 || this.decryptedMembers.length === 0 ||
this.decryptedMembers[0].member.memberId !== this.members[0].memberId this.decryptedMembers[0].member.memberId !== this.members[0].memberId
) { ) {
return "Your password is not the same as the organizer. Retry or have them check their password."; return "Your password is not the same as the organizer. Reload or have them check their password.";
} else { } else {
// the first (organizer) member was decrypted OK // the first (organizer) member was decrypted OK
return ""; return "";
@@ -342,7 +337,7 @@ export default class MembersList extends Vue {
group: "alert", group: "alert",
type: "info", type: "info",
title: "Contact Exists", title: "Contact Exists",
text: "They are in your contacts. To remove them, use the contacts page.", text: "They are in your contacts. If you want to remove them, you must do that from the contacts screen.",
}, },
10000, 10000,
); );
@@ -352,7 +347,7 @@ export default class MembersList extends Vue {
group: "alert", group: "alert",
type: "info", type: "info",
title: "Contact Available", title: "Contact Available",
text: "This is to add them to your contacts. To remove them later, use the contacts page.", text: "This is to add them to your contacts. If you want to remove them later, you must do that from the contacts screen.",
}, },
10000, 10000,
); );
@@ -360,16 +355,7 @@ export default class MembersList extends Vue {
} }
async loadContacts() { async loadContacts() {
const platformService = PlatformServiceFactory.getInstance(); this.contacts = await db.contacts.toArray();
const result = await platformService.dbQuery("SELECT * FROM contacts");
if (result) {
this.contacts = databaseUtil.mapQueryResultToValues(
result,
) as unknown as Contact[];
}
if (USE_DEXIE_DB) {
this.contacts = await db.contacts.toArray();
}
} }
getContactFor(did: string): Contact | undefined { getContactFor(did: string): Contact | undefined {
@@ -453,14 +439,7 @@ export default class MembersList extends Vue {
if (result.success) { if (result.success) {
decrMember.isRegistered = true; decrMember.isRegistered = true;
if (oldContact) { if (oldContact) {
const platformService = PlatformServiceFactory.getInstance(); await db.contacts.update(decrMember.did, { registered: true });
await platformService.dbExec(
"UPDATE contacts SET registered = ? WHERE did = ?",
[true, decrMember.did],
);
if (USE_DEXIE_DB) {
await db.contacts.update(decrMember.did, { registered: true });
}
oldContact.registered = true; oldContact.registered = true;
} }
this.$notify( this.$notify(
@@ -513,14 +492,7 @@ export default class MembersList extends Vue {
name: member.name, name: member.name,
}; };
const platformService = PlatformServiceFactory.getInstance(); await db.contacts.add(newContact);
await platformService.dbExec(
"INSERT INTO contacts (did, name) VALUES (?, ?)",
[member.did, member.name],
);
if (USE_DEXIE_DB) {
await db.contacts.add(newContact);
}
this.contacts.push(newContact); this.contacts.push(newContact);
this.$notify( this.$notify(

View File

@@ -82,13 +82,12 @@
<script lang="ts"> <script lang="ts">
import { Vue, Component, Prop } from "vue-facing-decorator"; import { Vue, Component, Prop } from "vue-facing-decorator";
import { NotificationIface, USE_DEXIE_DB } from "../constants/app"; import { NotificationIface } from "../constants/app";
import { import {
createAndSubmitOffer, createAndSubmitOffer,
serverMessageForUser, serverMessageForUser,
} from "../libs/endorserServer"; } from "../libs/endorserServer";
import * as libsUtil from "../libs/util"; import * as libsUtil from "../libs/util";
import * as databaseUtil from "../db/databaseUtil";
import { retrieveSettingsForActiveAccount } from "../db/index"; import { retrieveSettingsForActiveAccount } from "../db/index";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
@@ -117,10 +116,7 @@ export default class OfferDialog extends Vue {
this.recipientDid = recipientDid; this.recipientDid = recipientDid;
this.recipientName = recipientName; this.recipientName = recipientName;
let settings = await databaseUtil.retrieveSettingsForActiveAccount(); const settings = await retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.apiServer = settings.apiServer || ""; this.apiServer = settings.apiServer || "";
this.activeDid = settings.activeDid || ""; this.activeDid = settings.activeDid || "";
@@ -249,7 +245,10 @@ export default class OfferDialog extends Vue {
this.projectId, this.projectId,
); );
if (!result.success) { if (
result.type === "error" ||
this.isOfferCreationError(result.response)
) {
const errorMessage = this.getOfferCreationErrorMessage(result); const errorMessage = this.getOfferCreationErrorMessage(result);
logger.error("Error with offer creation result:", result); logger.error("Error with offer creation result:", result);
this.$notify( this.$notify(
@@ -293,6 +292,15 @@ export default class OfferDialog extends Vue {
// Helper functions for readability // Helper functions for readability
/**
* @param result response "data" from the server
* @returns true if the result indicates an error
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
isOfferCreationError(result: any) {
return result.status !== 201 || result.data?.error;
}
/** /**
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data") * @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
* @returns best guess at an error message * @returns best guess at an error message

View File

@@ -5,7 +5,7 @@
<h1 class="text-xl font-bold text-center mb-4 relative"> <h1 class="text-xl font-bold text-center mb-4 relative">
Welcome to Time Safari Welcome to Time Safari
<br /> <br />
- Showcase Impact & Magnify Time - Showcasing Gratitude & Magnifying Time
<div <div
class="text-lg text-center leading-none absolute right-0 -top-1" class="text-lg text-center leading-none absolute right-0 -top-1"
@click="onClickClose(true)" @click="onClickClose(true)"
@@ -14,9 +14,6 @@
</div> </div>
</h1> </h1>
The feed underneath this pop-up shows the latest contributions, some from
people and some from projects.
<p v-if="isRegistered" class="mt-4"> <p v-if="isRegistered" class="mt-4">
You can now log things that you've seen: You can now log things that you've seen:
<span v-if="numContacts > 0"> <span v-if="numContacts > 0">
@@ -26,10 +23,14 @@
<span class="bg-green-600 text-white rounded-full"> <span class="bg-green-600 text-white rounded-full">
<font-awesome icon="plus" class="fa-fw" /> <font-awesome icon="plus" class="fa-fw" />
</span> </span>
button to express your appreciation for... whatever. button to express your appreciation for... whatever -- maybe thanks for
showing you all these fascinating stories of
<em>gratitude</em>.
</p> </p>
<p class="mt-4"> <p v-else class="mt-4">
Once someone registers you, you can log your appreciation, too. The feed underneath this pop-up shows the latest gifts that others have
recognized. Once someone registers you, you can log your appreciation,
too.
</p> </p>
<p class="mt-4"> <p class="mt-4">
@@ -200,16 +201,13 @@
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router"; import { Router } from "vue-router";
import { NotificationIface, USE_DEXIE_DB } from "../constants/app"; import { NotificationIface } from "../constants/app";
import { import {
db, db,
retrieveSettingsForActiveAccount, retrieveSettingsForActiveAccount,
updateAccountSettings, updateAccountSettings,
} from "../db/index"; } from "../db/index";
import * as databaseUtil from "../db/databaseUtil";
import { OnboardPage } from "../libs/util"; import { OnboardPage } from "../libs/util";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { Contact } from "@/db/tables/contacts";
@Component({ @Component({
computed: { computed: {
@@ -224,7 +222,7 @@ export default class OnboardingDialog extends Vue {
$router!: Router; $router!: Router;
activeDid = ""; activeDid = "";
firstContactName = ""; firstContactName = null;
givenName = ""; givenName = "";
isRegistered = false; isRegistered = false;
numContacts = 0; numContacts = 0;
@@ -233,54 +231,29 @@ export default class OnboardingDialog extends Vue {
async open(page: OnboardPage) { async open(page: OnboardPage) {
this.page = page; this.page = page;
let settings = await databaseUtil.retrieveSettingsForActiveAccount(); const settings = await retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.activeDid = settings.activeDid || ""; this.activeDid = settings.activeDid || "";
this.isRegistered = !!settings.isRegistered; this.isRegistered = !!settings.isRegistered;
const platformService = PlatformServiceFactory.getInstance(); const contacts = await db.contacts.toArray();
const dbContacts = await platformService.dbQuery("SELECT * FROM contacts"); this.numContacts = contacts.length;
if (dbContacts) { if (this.numContacts > 0) {
this.numContacts = dbContacts.values.length; this.firstContactName = contacts[0].name;
const firstContact = dbContacts.values[0];
const fullContact = databaseUtil.mapColumnsToValues(dbContacts.columns, [
firstContact,
]) as unknown as Contact;
this.firstContactName = fullContact.name || "";
}
if (USE_DEXIE_DB) {
const contacts = await db.contacts.toArray();
this.numContacts = contacts.length;
if (this.numContacts > 0) {
this.firstContactName = contacts[0].name || "";
}
} }
this.visible = true; this.visible = true;
if (this.page === OnboardPage.Create) { if (this.page === OnboardPage.Create) {
// we'll assume that they've been through all the other pages // we'll assume that they've been through all the other pages
await databaseUtil.updateDidSpecificSettings(this.activeDid, { await updateAccountSettings(this.activeDid, {
finishedOnboarding: true, finishedOnboarding: true,
}); });
if (USE_DEXIE_DB) {
await updateAccountSettings(this.activeDid, {
finishedOnboarding: true,
});
}
} }
} }
async onClickClose(done?: boolean, goHome?: boolean) { async onClickClose(done?: boolean, goHome?: boolean) {
this.visible = false; this.visible = false;
if (done) { if (done) {
await databaseUtil.updateDidSpecificSettings(this.activeDid, { await updateAccountSettings(this.activeDid, {
finishedOnboarding: true, finishedOnboarding: true,
}); });
if (USE_DEXIE_DB) {
await updateAccountSettings(this.activeDid, {
finishedOnboarding: true,
});
}
if (goHome) { if (goHome) {
this.$router.push({ name: "home" }); this.$router.push({ name: "home" });
} }

View File

@@ -1,114 +0,0 @@
/** * PersonCard.vue - Individual person display component * * Extracted from
GiftedDialog.vue to handle person entity display * with selection states and
conflict detection. * * @author Matthew Raymer */
<template>
<li :class="cardClasses" @click="handleClick">
<div class="relative w-fit mx-auto">
<EntityIcon
v-if="person.did"
:contact="person"
class="!size-[3rem] mx-auto border border-slate-300 bg-white overflow-hidden rounded-full mb-1"
/>
<font-awesome
v-else
icon="circle-question"
class="text-slate-400 text-5xl mb-1"
/>
<!-- Time icon overlay for contacts -->
<div
v-if="person.did && showTimeIcon"
class="rounded-full bg-slate-400 absolute bottom-0 right-0 p-1 translate-x-1/3"
>
<font-awesome icon="clock" class="block text-white text-xs w-[1em]" />
</div>
</div>
<h3 :class="nameClasses">
{{ person.name || person.did || "Unnamed" }}
</h3>
</li>
</template>
<script lang="ts">
import { Component, Prop, Vue, Emit } from "vue-facing-decorator";
import EntityIcon from "./EntityIcon.vue";
import { Contact } from "../db/tables/contacts";
/**
* PersonCard - Individual person display with selection capability
*
* Features:
* - Person avatar using EntityIcon
* - Selection states (selectable, conflicted, disabled)
* - Time icon overlay for contacts
* - Click event handling
* - Emits click events for parent handling
*/
@Component({
components: {
EntityIcon,
},
})
export default class PersonCard extends Vue {
/** Contact data to display */
@Prop({ required: true })
person!: Contact;
/** Whether this person can be selected */
@Prop({ default: true })
selectable!: boolean;
/** Whether this person would create a conflict if selected */
@Prop({ default: false })
conflicted!: boolean;
/** Whether to show time icon overlay */
@Prop({ default: false })
showTimeIcon!: boolean;
/**
* Computed CSS classes for the card
*/
get cardClasses(): string {
if (!this.selectable || this.conflicted) {
return "opacity-50 cursor-not-allowed";
}
return "cursor-pointer hover:bg-slate-50";
}
/**
* Computed CSS classes for the person name
*/
get nameClasses(): string {
const baseClasses =
"text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden";
if (this.conflicted) {
return `${baseClasses} text-slate-400`;
}
return baseClasses;
}
/**
* Handle card click - only emit if selectable and not conflicted
*/
handleClick(): void {
if (this.selectable && !this.conflicted) {
this.emitPersonSelected(this.person);
}
}
// Emit methods using @Emit decorator
@Emit("person-selected")
emitPersonSelected(person: Contact): Contact {
return person;
}
}
</script>
<style scoped>
/* Component-specific styles if needed */
</style>

View File

@@ -119,12 +119,7 @@ PhotoDialog.vue */
import axios from "axios"; import axios from "axios";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import VuePictureCropper, { cropper } from "vue-picture-cropper"; import VuePictureCropper, { cropper } from "vue-picture-cropper";
import { import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
DEFAULT_IMAGE_API_SERVER,
NotificationIface,
USE_DEXIE_DB,
} from "../constants/app";
import * as databaseUtil from "../db/databaseUtil";
import { retrieveSettingsForActiveAccount } from "../db/index"; import { retrieveSettingsForActiveAccount } from "../db/index";
import { accessToken } from "../libs/crypto"; import { accessToken } from "../libs/crypto";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
@@ -178,12 +173,9 @@ export default class PhotoDialog extends Vue {
* @throws {Error} When settings retrieval fails * @throws {Error} When settings retrieval fails
*/ */
async mounted() { async mounted() {
// logger.log("PhotoDialog mounted"); logger.log("PhotoDialog mounted");
try { try {
let settings = await databaseUtil.retrieveSettingsForActiveAccount(); const settings = await retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.activeDid = settings.activeDid || ""; this.activeDid = settings.activeDid || "";
this.isRegistered = !!settings.isRegistered; this.isRegistered = !!settings.isRegistered;
logger.log("isRegistered:", this.isRegistered); logger.log("isRegistered:", this.isRegistered);

View File

@@ -1,96 +0,0 @@
/** * ProjectCard.vue - Individual project display component * * Extracted from
GiftedDialog.vue to handle project entity display * with selection states and
issuer information. * * @author Matthew Raymer */
<template>
<li class="cursor-pointer" @click="handleClick">
<div class="relative w-fit mx-auto">
<ProjectIcon
:entity-id="project.handleId"
:icon-size="48"
:image-url="project.image"
class="!size-[3rem] mx-auto border border-slate-300 bg-white overflow-hidden rounded-full mb-1"
/>
</div>
<h3
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
>
{{ project.name }}
</h3>
<div class="text-xs text-slate-500 truncate">
<font-awesome icon="user" class="fa-fw text-slate-400" />
{{ issuerDisplayName }}
</div>
</li>
</template>
<script lang="ts">
import { Component, Prop, Vue, Emit } from "vue-facing-decorator";
import ProjectIcon from "./ProjectIcon.vue";
import { PlanData } from "../interfaces/records";
import { Contact } from "../db/tables/contacts";
import { didInfo } from "../libs/endorserServer";
/**
* ProjectCard - Displays a project entity with selection capability
*
* Features:
* - Shows project icon using ProjectIcon
* - Displays project name and issuer information
* - Handles click events for selection
* - Shows issuer name using didInfo utility
*/
@Component({
components: {
ProjectIcon,
},
})
export default class ProjectCard extends Vue {
/** Project entity to display */
@Prop({ required: true })
project!: PlanData;
/** Active user's DID for issuer display */
@Prop({ required: true })
activeDid!: string;
/** All user's DIDs for issuer display */
@Prop({ required: true })
allMyDids!: string[];
/** All contacts for issuer display */
@Prop({ required: true })
allContacts!: Contact[];
/**
* Computed display name for the project issuer
*/
get issuerDisplayName(): string {
return didInfo(
this.project.issuerDid,
this.activeDid,
this.allMyDids,
this.allContacts,
);
}
/**
* Handle card click - emit project selection
*/
handleClick(): void {
this.emitProjectSelected(this.project);
}
// Emit methods using @Emit decorator
@Emit("project-selected")
emitProjectSelected(project: PlanData): PlanData {
return project;
}
}
</script>
<style scoped>
/* Component-specific styles if needed */
</style>

View File

@@ -1,14 +1,18 @@
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<template> <template>
<a <a
v-if="linkToFullImage && imageUrl" v-if="linkToFull && imageUrl"
:href="imageUrl" :href="imageUrl"
target="_blank" target="_blank"
class="h-full w-full object-contain" class="h-full w-full object-contain"
> >
<div class="h-full w-full object-contain" v-html="generateIcon()" /> <div class="h-full w-full object-contain" v-html="generateIdenticon()" />
</a> </a>
<div v-else class="h-full w-full object-contain" v-html="generateIcon()" /> <div
v-else
class="h-full w-full object-contain"
v-html="generateIdenticon()"
/>
</template> </template>
<script lang="ts"> <script lang="ts">
import { toSvg } from "jdenticon"; import { toSvg } from "jdenticon";
@@ -31,9 +35,9 @@ export default class ProjectIcon extends Vue {
@Prop entityId = ""; @Prop entityId = "";
@Prop iconSize = 0; @Prop iconSize = 0;
@Prop imageUrl = ""; @Prop imageUrl = "";
@Prop linkToFullImage = false; @Prop linkToFull = false;
generateIcon() { generateIdenticon() {
if (this.imageUrl) { if (this.imageUrl) {
return `<img src="${this.imageUrl}" class="w-full h-full object-contain" />`; return `<img src="${this.imageUrl}" class="w-full h-full object-contain" />`;
} else { } else {

View File

@@ -102,12 +102,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { import { DEFAULT_PUSH_SERVER, NotificationIface } from "../constants/app";
DEFAULT_PUSH_SERVER,
NotificationIface,
USE_DEXIE_DB,
} from "../constants/app";
import * as databaseUtil from "../db/databaseUtil";
import { import {
logConsoleAndDb, logConsoleAndDb,
retrieveSettingsForActiveAccount, retrieveSettingsForActiveAccount,
@@ -174,10 +169,7 @@ export default class PushNotificationPermission extends Vue {
this.isVisible = true; this.isVisible = true;
this.pushType = pushType; this.pushType = pushType;
try { try {
let settings = await databaseUtil.retrieveSettingsForActiveAccount(); const settings = await retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
let pushUrl = DEFAULT_PUSH_SERVER; let pushUrl = DEFAULT_PUSH_SERVER;
if (settings?.webPushServer) { if (settings?.webPushServer) {
pushUrl = settings.webPushServer; pushUrl = settings.webPushServer;

View File

@@ -1,66 +0,0 @@
/** * ShowAllCard.vue - Show All navigation card component * * Extracted from
GiftedDialog.vue to handle "Show All" navigation * for both people and projects
entity types. * * @author Matthew Raymer */
<template>
<li class="cursor-pointer">
<router-link :to="navigationRoute" class="block text-center">
<font-awesome icon="circle-right" class="text-blue-500 text-5xl mb-1" />
<h3
class="text-xs text-slate-500 font-medium italic text-ellipsis whitespace-nowrap overflow-hidden"
>
Show All
</h3>
</router-link>
</li>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-facing-decorator";
import { RouteLocationRaw } from "vue-router";
/**
* ShowAllCard - Displays "Show All" navigation for entity grids
*
* Features:
* - Provides navigation to full entity listings
* - Supports different routes based on entity type
* - Maintains context through query parameters
* - Consistent visual styling with other cards
*/
@Component
export default class ShowAllCard extends Vue {
/** Type of entities being shown */
@Prop({ required: true })
entityType!: "people" | "projects";
/** Route name to navigate to */
@Prop({ required: true })
routeName!: string;
/** Query parameters to pass to the route */
@Prop({ default: () => ({}) })
queryParams!: Record<string, any>;
/**
* Computed navigation route with query parameters
*/
get navigationRoute(): RouteLocationRaw {
return {
name: this.routeName,
query: this.queryParams,
};
}
}
</script>
<style scoped>
/* Ensure router-link styling is consistent */
a {
text-decoration: none;
}
a:hover .fa-circle-right {
transform: scale(1.1);
transition: transform 0.2s ease;
}
</style>

View File

@@ -1,135 +0,0 @@
/** * SpecialEntityCard.vue - Special entity display component * * Extracted
from GiftedDialog.vue to handle special entities like "You" * and "Unnamed" with
conflict detection and selection capability. * * @author Matthew Raymer */
<template>
<li :class="cardClasses" @click="handleClick">
<font-awesome :icon="icon" :class="iconClasses" />
<h3 :class="nameClasses">
{{ label }}
</h3>
</li>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-facing-decorator";
import { Emit } from "vue-facing-decorator";
/**
* SpecialEntityCard - Displays special entities with selection capability
*
* Features:
* - Displays special entities like "You" and "Unnamed"
* - Shows appropriate FontAwesome icons
* - Handles conflict states and selection
* - Emits selection events with entity data
* - Configurable styling based on entity type
*/
@Component({
emits: ["entity-selected"],
})
export default class SpecialEntityCard extends Vue {
/** Type of special entity */
@Prop({ required: true })
entityType!: "you" | "unnamed";
/** Display label for the entity */
@Prop({ required: true })
label!: string;
/** FontAwesome icon name */
@Prop({ required: true })
icon!: string;
/** Whether this entity can be selected */
@Prop({ default: true })
selectable!: boolean;
/** Whether selecting this entity would create a conflict */
@Prop({ default: false })
conflicted!: boolean;
/** Entity data to emit when selected */
@Prop({ required: true })
entityData!: { did?: string; name: string };
/**
* Computed CSS classes for the card container
*/
get cardClasses(): string {
const baseClasses = "block";
if (!this.selectable || this.conflicted) {
return `${baseClasses} cursor-not-allowed opacity-50`;
}
return `${baseClasses} cursor-pointer`;
}
/**
* Computed CSS classes for the icon
*/
get iconClasses(): string {
const baseClasses = "text-5xl mb-1";
if (this.conflicted) {
return `${baseClasses} text-slate-400`;
}
// Different colors for different entity types
switch (this.entityType) {
case "you":
return `${baseClasses} text-blue-500`;
case "unnamed":
return `${baseClasses} text-slate-400`;
default:
return `${baseClasses} text-slate-400`;
}
}
/**
* Computed CSS classes for the entity name/label
*/
get nameClasses(): string {
const baseClasses =
"text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden";
if (this.conflicted) {
return `${baseClasses} text-slate-400`;
}
// Different colors for different entity types
switch (this.entityType) {
case "you":
return `${baseClasses} text-blue-500`;
case "unnamed":
return `${baseClasses} text-slate-500 italic`;
default:
return `${baseClasses} text-slate-500`;
}
}
/**
* Handle card click - only emit if selectable and not conflicted
*/
handleClick(): void {
if (this.selectable && !this.conflicted) {
this.emitEntitySelected({
type: "special",
entityType: this.entityType,
data: this.entityData,
});
}
}
// Emit methods using @Emit decorator
@Emit("entity-selected")
emitEntitySelected(data: any): any {
return data;
}
}
</script>
<style scoped>
/* Component-specific styles if needed */
</style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="absolute right-5 top-[max(0.75rem,env(safe-area-inset-top))]"> <div class="absolute right-5 top-[calc(env(safe-area-inset-top)+0.75rem)]">
<span class="align-center text-red-500 mr-2">{{ message }}</span> <span class="align-center text-red-500 mr-2">{{ message }}</span>
<span class="ml-2"> <span class="ml-2">
<router-link <router-link
@@ -15,8 +15,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue, Prop } from "vue-facing-decorator"; import { Component, Vue, Prop } from "vue-facing-decorator";
import { AppString, NotificationIface, USE_DEXIE_DB } from "../constants/app"; import { AppString, NotificationIface } from "../constants/app";
import * as databaseUtil from "../db/databaseUtil";
import { retrieveSettingsForActiveAccount } from "../db/index"; import { retrieveSettingsForActiveAccount } from "../db/index";
@Component @Component
@@ -29,10 +28,7 @@ export default class TopMessage extends Vue {
async mounted() { async mounted() {
try { try {
let settings = await databaseUtil.retrieveSettingsForActiveAccount(); const settings = await retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
if ( if (
settings.warnIfTestServer && settings.warnIfTestServer &&
settings.apiServer !== AppString.PROD_ENDORSER_API_SERVER settings.apiServer !== AppString.PROD_ENDORSER_API_SERVER

Some files were not shown because too many files have changed in this diff Show More