Compare commits
2 Commits
matthew-sc
...
sql-wa-sql
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d7bc5f728 | |||
| 753d178fc3 |
@@ -1,153 +0,0 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
# Absurd SQL - Cursor Development Guide
|
||||
|
||||
## Project Overview
|
||||
Absurd SQL is a backend implementation for sql.js that enables persistent SQLite databases in the browser by using IndexedDB as a block storage system. This guide provides rules and best practices for developing with this project in Cursor.
|
||||
|
||||
## Project Structure
|
||||
```
|
||||
absurd-sql/
|
||||
├── src/ # Source code
|
||||
├── dist/ # Built files
|
||||
├── package.json # Dependencies and scripts
|
||||
├── rollup.config.js # Build configuration
|
||||
└── jest.config.js # Test configuration
|
||||
```
|
||||
|
||||
## Development Rules
|
||||
|
||||
### 1. Worker Thread Requirements
|
||||
- All SQL operations MUST be performed in a worker thread
|
||||
- Main thread should only handle worker initialization and communication
|
||||
- Never block the main thread with database operations
|
||||
|
||||
### 2. Code Organization
|
||||
- Keep worker code in separate files (e.g., `*.worker.js`)
|
||||
- Use ES modules for imports/exports
|
||||
- Follow the project's existing module structure
|
||||
|
||||
### 3. Required Headers
|
||||
When developing locally or deploying, ensure these headers are set:
|
||||
```
|
||||
Cross-Origin-Opener-Policy: same-origin
|
||||
Cross-Origin-Embedder-Policy: require-corp
|
||||
```
|
||||
|
||||
### 4. Browser Compatibility
|
||||
- Primary target: Modern browsers with SharedArrayBuffer support
|
||||
- Fallback mode: Safari (with limitations)
|
||||
- Always test in both modes
|
||||
|
||||
### 5. Database Configuration
|
||||
Recommended database settings:
|
||||
```sql
|
||||
PRAGMA journal_mode=MEMORY;
|
||||
PRAGMA page_size=8192; -- Optional, but recommended
|
||||
```
|
||||
|
||||
### 6. Development Workflow
|
||||
1. Install dependencies:
|
||||
```bash
|
||||
yarn add @jlongster/sql.js absurd-sql
|
||||
```
|
||||
|
||||
2. Development commands:
|
||||
- `yarn build` - Build the project
|
||||
- `yarn jest` - Run tests
|
||||
- `yarn serve` - Start development server
|
||||
|
||||
### 7. Testing Guidelines
|
||||
- Write tests for both SharedArrayBuffer and fallback modes
|
||||
- Use Jest for testing
|
||||
- Include performance benchmarks for critical operations
|
||||
|
||||
### 8. Performance Considerations
|
||||
- Use bulk operations when possible
|
||||
- Monitor read/write performance
|
||||
- Consider using transactions for multiple operations
|
||||
- Avoid unnecessary database connections
|
||||
|
||||
### 9. Error Handling
|
||||
- Implement proper error handling for:
|
||||
- Worker initialization failures
|
||||
- Database connection issues
|
||||
- Concurrent access conflicts (in fallback mode)
|
||||
- Storage quota exceeded scenarios
|
||||
|
||||
### 10. Security Best Practices
|
||||
- Never expose database operations directly to the client
|
||||
- Validate all SQL queries
|
||||
- Implement proper access controls
|
||||
- Handle sensitive data appropriately
|
||||
|
||||
### 11. Code Style
|
||||
- Follow ESLint configuration
|
||||
- Use async/await for asynchronous operations
|
||||
- Document complex database operations
|
||||
- Include comments for non-obvious optimizations
|
||||
|
||||
### 12. Debugging
|
||||
- Use `jest-debug` for debugging tests
|
||||
- Monitor IndexedDB usage in browser dev tools
|
||||
- Check worker communication in console
|
||||
- Use performance monitoring tools
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Worker Initialization
|
||||
```javascript
|
||||
// Main thread
|
||||
import { initBackend } from 'absurd-sql/dist/indexeddb-main-thread';
|
||||
|
||||
function init() {
|
||||
let worker = new Worker(new URL('./index.worker.js', import.meta.url));
|
||||
initBackend(worker);
|
||||
}
|
||||
```
|
||||
|
||||
### Database Setup
|
||||
```javascript
|
||||
// Worker thread
|
||||
import initSqlJs from '@jlongster/sql.js';
|
||||
import { SQLiteFS } from 'absurd-sql';
|
||||
import IndexedDBBackend from 'absurd-sql/dist/indexeddb-backend';
|
||||
|
||||
async function setupDatabase() {
|
||||
let SQL = await initSqlJs({ locateFile: file => file });
|
||||
let sqlFS = new SQLiteFS(SQL.FS, new IndexedDBBackend());
|
||||
SQL.register_for_idb(sqlFS);
|
||||
|
||||
SQL.FS.mkdir('/sql');
|
||||
SQL.FS.mount(sqlFS, {}, '/sql');
|
||||
|
||||
return new SQL.Database('/sql/db.sqlite', { filename: true });
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
1. SharedArrayBuffer not available
|
||||
- Check COOP/COEP headers
|
||||
- Verify browser support
|
||||
- Test fallback mode
|
||||
|
||||
2. Worker initialization failures
|
||||
- Check file paths
|
||||
- Verify module imports
|
||||
- Check browser console for errors
|
||||
|
||||
3. Performance issues
|
||||
- Monitor IndexedDB usage
|
||||
- Check for unnecessary operations
|
||||
- Verify transaction usage
|
||||
|
||||
## Resources
|
||||
- [Project Demo](https://priceless-keller-d097e5.netlify.app/)
|
||||
- [Example Project](https://github.com/jlongster/absurd-example-project)
|
||||
- [Blog Post](https://jlongster.com/future-sql-web)
|
||||
- [SQL.js Documentation](https://github.com/sql-js/sql.js/)
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
alwaysApply: true
|
||||
---
|
||||
# Camera Implementation Documentation
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
use the system date function to understand the proper date and time for all interactions.
|
||||
@@ -1,267 +0,0 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
# wa-sqlite Usage Guide
|
||||
|
||||
## Table of Contents
|
||||
- [1. Overview](#1-overview)
|
||||
- [2. Installation](#2-installation)
|
||||
- [3. Basic Setup](#3-basic-setup)
|
||||
- [3.1 Import and Initialize](#31-import-and-initialize)
|
||||
- [3.2 Basic Database Operations](#32-basic-database-operations)
|
||||
- [4. Virtual File Systems (VFS)](#4-virtual-file-systems-vfs)
|
||||
- [4.1 Available VFS Options](#41-available-vfs-options)
|
||||
- [4.2 Using a VFS](#42-using-a-vfs)
|
||||
- [5. Best Practices](#5-best-practices)
|
||||
- [5.1 Error Handling](#51-error-handling)
|
||||
- [5.2 Transaction Management](#52-transaction-management)
|
||||
- [5.3 Prepared Statements](#53-prepared-statements)
|
||||
- [6. Performance Considerations](#6-performance-considerations)
|
||||
- [7. Common Issues and Solutions](#7-common-issues-and-solutions)
|
||||
- [8. TypeScript Support](#8-typescript-support)
|
||||
|
||||
## 1. Overview
|
||||
wa-sqlite is a WebAssembly build of SQLite that enables SQLite database operations in web browsers and JavaScript environments. It provides both synchronous and asynchronous builds, with support for custom virtual file systems (VFS) for persistent storage.
|
||||
|
||||
## 2. Installation
|
||||
```bash
|
||||
npm install wa-sqlite
|
||||
# or
|
||||
yarn add wa-sqlite
|
||||
```
|
||||
|
||||
## 3. Basic Setup
|
||||
|
||||
### 3.1 Import and Initialize
|
||||
```javascript
|
||||
// Choose one of these imports based on your needs:
|
||||
// - wa-sqlite.mjs: Synchronous build
|
||||
// - wa-sqlite-async.mjs: Asynchronous build (required for async VFS)
|
||||
// - wa-sqlite-jspi.mjs: JSPI-based async build (experimental, Chromium only)
|
||||
import SQLiteESMFactory from 'wa-sqlite/dist/wa-sqlite.mjs';
|
||||
import * as SQLite from 'wa-sqlite';
|
||||
|
||||
async function initDatabase() {
|
||||
// Initialize SQLite module
|
||||
const module = await SQLiteESMFactory();
|
||||
const sqlite3 = SQLite.Factory(module);
|
||||
|
||||
// Open database (returns a Promise)
|
||||
const db = await sqlite3.open_v2('myDatabase');
|
||||
return { sqlite3, db };
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Basic Database Operations
|
||||
```javascript
|
||||
async function basicOperations() {
|
||||
const { sqlite3, db } = await initDatabase();
|
||||
|
||||
try {
|
||||
// Create a table
|
||||
await sqlite3.exec(db, `
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT UNIQUE
|
||||
)
|
||||
`);
|
||||
|
||||
// Insert data
|
||||
await sqlite3.exec(db, `
|
||||
INSERT INTO users (name, email)
|
||||
VALUES ('John Doe', 'john@example.com')
|
||||
`);
|
||||
|
||||
// Query data
|
||||
const results = [];
|
||||
await sqlite3.exec(db, 'SELECT * FROM users', (row, columns) => {
|
||||
results.push({ row, columns });
|
||||
});
|
||||
|
||||
return results;
|
||||
} finally {
|
||||
// Always close the database when done
|
||||
await sqlite3.close(db);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Virtual File Systems (VFS)
|
||||
|
||||
### 4.1 Available VFS Options
|
||||
wa-sqlite provides several VFS implementations for persistent storage:
|
||||
|
||||
1. **IDBBatchAtomicVFS** (Recommended for general use)
|
||||
- Uses IndexedDB with batch atomic writes
|
||||
- Works in all contexts (Window, Worker, Service Worker)
|
||||
- Supports WAL mode
|
||||
- Best performance with `PRAGMA synchronous=normal`
|
||||
|
||||
2. **IDBMirrorVFS**
|
||||
- Keeps files in memory, persists to IndexedDB
|
||||
- Works in all contexts
|
||||
- Good for smaller databases
|
||||
|
||||
3. **OPFS-based VFS** (Origin Private File System)
|
||||
- Various implementations available:
|
||||
- AccessHandlePoolVFS
|
||||
- OPFSAdaptiveVFS
|
||||
- OPFSCoopSyncVFS
|
||||
- OPFSPermutedVFS
|
||||
- Better performance but limited to Worker contexts
|
||||
|
||||
### 4.2 Using a VFS
|
||||
```javascript
|
||||
import { IDBBatchAtomicVFS } from 'wa-sqlite/src/examples/IDBBatchAtomicVFS.js';
|
||||
import SQLiteESMFactory from 'wa-sqlite/dist/wa-sqlite-async.mjs';
|
||||
import * as SQLite from 'wa-sqlite';
|
||||
|
||||
async function initDatabaseWithVFS() {
|
||||
const module = await SQLiteESMFactory();
|
||||
const sqlite3 = SQLite.Factory(module);
|
||||
|
||||
// Register VFS
|
||||
const vfs = await IDBBatchAtomicVFS.create('myApp', module);
|
||||
sqlite3.vfs_register(vfs, true);
|
||||
|
||||
// Open database with VFS
|
||||
const db = await sqlite3.open_v2('myDatabase');
|
||||
|
||||
// Configure for better performance
|
||||
await sqlite3.exec(db, 'PRAGMA synchronous = normal');
|
||||
await sqlite3.exec(db, 'PRAGMA journal_mode = WAL');
|
||||
|
||||
return { sqlite3, db };
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Best Practices
|
||||
|
||||
### 5.1 Error Handling
|
||||
```javascript
|
||||
async function safeDatabaseOperation() {
|
||||
const { sqlite3, db } = await initDatabase();
|
||||
|
||||
try {
|
||||
await sqlite3.exec(db, 'SELECT * FROM non_existent_table');
|
||||
} catch (error) {
|
||||
if (error.code === SQLite.SQLITE_ERROR) {
|
||||
console.error('SQL error:', error.message);
|
||||
} else {
|
||||
console.error('Database error:', error);
|
||||
}
|
||||
} finally {
|
||||
await sqlite3.close(db);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 Transaction Management
|
||||
```javascript
|
||||
async function transactionExample() {
|
||||
const { sqlite3, db } = await initDatabase();
|
||||
|
||||
try {
|
||||
await sqlite3.exec(db, 'BEGIN TRANSACTION');
|
||||
|
||||
// Perform multiple operations
|
||||
await sqlite3.exec(db, 'INSERT INTO users (name) VALUES (?)', ['Alice']);
|
||||
await sqlite3.exec(db, 'INSERT INTO users (name) VALUES (?)', ['Bob']);
|
||||
|
||||
await sqlite3.exec(db, 'COMMIT');
|
||||
} catch (error) {
|
||||
await sqlite3.exec(db, 'ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
await sqlite3.close(db);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 Prepared Statements
|
||||
```javascript
|
||||
async function preparedStatementExample() {
|
||||
const { sqlite3, db } = await initDatabase();
|
||||
|
||||
try {
|
||||
// Prepare statement
|
||||
const stmt = await sqlite3.prepare(db, 'SELECT * FROM users WHERE id = ?');
|
||||
|
||||
// Execute with different parameters
|
||||
await sqlite3.bind(stmt, 1, 1);
|
||||
while (await sqlite3.step(stmt) === SQLite.SQLITE_ROW) {
|
||||
const row = sqlite3.row(stmt);
|
||||
console.log(row);
|
||||
}
|
||||
|
||||
// Reset and reuse
|
||||
await sqlite3.reset(stmt);
|
||||
await sqlite3.bind(stmt, 1, 2);
|
||||
// ... execute again
|
||||
|
||||
await sqlite3.finalize(stmt);
|
||||
} finally {
|
||||
await sqlite3.close(db);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Performance Considerations
|
||||
|
||||
1. **VFS Selection**
|
||||
- Use IDBBatchAtomicVFS for general-purpose applications
|
||||
- Consider OPFS-based VFS for better performance in Worker contexts
|
||||
- Use MemoryVFS for temporary databases
|
||||
|
||||
2. **Configuration**
|
||||
- Set appropriate page size (default is usually fine)
|
||||
- Use WAL mode for better concurrency
|
||||
- Consider `PRAGMA synchronous=normal` for better performance
|
||||
- Adjust cache size based on your needs
|
||||
|
||||
3. **Concurrency**
|
||||
- Use transactions for multiple operations
|
||||
- Be aware of VFS-specific concurrency limitations
|
||||
- Consider using Web Workers for heavy database operations
|
||||
|
||||
## 7. Common Issues and Solutions
|
||||
|
||||
1. **Database Locking**
|
||||
- Use appropriate transaction isolation levels
|
||||
- Implement retry logic for busy errors
|
||||
- Consider using WAL mode
|
||||
|
||||
2. **Storage Limitations**
|
||||
- Be aware of browser storage quotas
|
||||
- Implement cleanup strategies
|
||||
- Monitor database size
|
||||
|
||||
3. **Cross-Context Access**
|
||||
- Use appropriate VFS for your context
|
||||
- Consider message passing for cross-context communication
|
||||
- Be aware of storage access limitations
|
||||
|
||||
## 8. TypeScript Support
|
||||
wa-sqlite includes TypeScript definitions. The main types are:
|
||||
|
||||
```typescript
|
||||
type SQLiteCompatibleType = number | string | Uint8Array | Array<number> | bigint | null;
|
||||
|
||||
interface SQLiteAPI {
|
||||
open_v2(filename: string, flags?: number, zVfs?: string): Promise<number>;
|
||||
exec(db: number, sql: string, callback?: (row: any[], columns: string[]) => void): Promise<number>;
|
||||
close(db: number): Promise<number>;
|
||||
// ... other methods
|
||||
}
|
||||
```
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Official GitHub Repository](https://github.com/rhashimoto/wa-sqlite)
|
||||
- [Online Demo](https://rhashimoto.github.io/wa-sqlite/demo/)
|
||||
- [API Reference](https://rhashimoto.github.io/wa-sqlite/docs/)
|
||||
- [FAQ](https://github.com/rhashimoto/wa-sqlite/issues?q=is%3Aissue+label%3Afaq+)
|
||||
- [Discussion Forums](https://github.com/rhashimoto/wa-sqlite/discussions)
|
||||
@@ -2,12 +2,11 @@
|
||||
|
||||
# iOS doesn't like spaces in the app title.
|
||||
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).
|
||||
VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F
|
||||
VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000
|
||||
# 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_PARTNER_API_SERVER=http://localhost:3000
|
||||
#VITE_DEFAULT_PUSH_SERVER... can't be set up with localhost domain
|
||||
VITE_PASSKEYS_ENABLED=true
|
||||
|
||||
6
.env.example
Normal 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
|
||||
@@ -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_PARTNER_API_SERVER=https://partner-api.endorser.ch
|
||||
VITE_DEFAULT_PUSH_SERVER=https://timesafari.app
|
||||
|
||||
@@ -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_PARTNER_API_SERVER=https://test-partner-api.endorser.ch
|
||||
VITE_DEFAULT_PUSH_SERVER=https://test.timesafari.app
|
||||
VITE_PASSKEYS_ENABLED=true
|
||||
|
||||
@@ -4,12 +4,6 @@ module.exports = {
|
||||
node: true,
|
||||
es2022: true,
|
||||
},
|
||||
ignorePatterns: [
|
||||
'node_modules/',
|
||||
'dist/',
|
||||
'dist-electron/',
|
||||
'*.d.ts'
|
||||
],
|
||||
extends: [
|
||||
"plugin:vue/vue3-recommended",
|
||||
"eslint:recommended",
|
||||
|
||||
5
.gitignore
vendored
@@ -51,7 +51,6 @@ vendor/
|
||||
# Build logs
|
||||
build_logs/
|
||||
|
||||
# PWA icon files generated by capacitor-assets
|
||||
icons
|
||||
|
||||
android/app/src/main/assets/public
|
||||
android/app/src/main/res
|
||||
|
||||
|
||||
75
BUILDING.md
@@ -9,6 +9,19 @@ For a quick dev environment setup, use [pkgx](https://pkgx.dev).
|
||||
- Node.js (LTS version recommended)
|
||||
- npm (comes with Node.js)
|
||||
- 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
|
||||
|
||||
## Forks
|
||||
@@ -71,7 +84,7 @@ Install dependencies:
|
||||
* For test, build the app (because test server is not yet set up to build):
|
||||
|
||||
```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:
|
||||
@@ -228,9 +241,7 @@ docker run -d \
|
||||
1. Build the electron app in production mode:
|
||||
|
||||
```bash
|
||||
npm run build:web
|
||||
npm run build:electron
|
||||
npm run electron:build-mac
|
||||
npm run build:electron-prod
|
||||
```
|
||||
|
||||
2. Package the Electron app for macOS:
|
||||
@@ -313,32 +324,6 @@ npm run build:electron-prod && npm run electron:start
|
||||
|
||||
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:
|
||||
|
||||
```bash
|
||||
@@ -347,7 +332,6 @@ Prerequisites: macOS with Xcode installed
|
||||
npm run build:capacitor
|
||||
```
|
||||
|
||||
|
||||
2. Update iOS project with latest build:
|
||||
|
||||
```bash
|
||||
@@ -359,11 +343,7 @@ Prerequisites: macOS with Xcode installed
|
||||
3. Copy the assets:
|
||||
|
||||
```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
|
||||
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
|
||||
```
|
||||
|
||||
@@ -371,10 +351,10 @@ Prerequisites: macOS with Xcode installed
|
||||
|
||||
```
|
||||
cd ios/App
|
||||
xcrun agvtool new-version 25
|
||||
xcrun agvtool new-version 15
|
||||
# Unfortunately this edits Info.plist directly.
|
||||
#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
|
||||
cd -
|
||||
```
|
||||
@@ -387,25 +367,28 @@ Prerequisites: macOS with Xcode installed
|
||||
|
||||
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
|
||||
|
||||
* Someday: Under "General" we want to rename a bunch of things to "Time Safari"
|
||||
* Choose Product -> Destination -> Any iOS Device
|
||||
* Under "General" renamed a bunch of things to "Time Safari"
|
||||
* Choose Product -> Destination -> Build Any iOS
|
||||
* 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`).
|
||||
* 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.
|
||||
* 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.
|
||||
* You'll probably have to "Manage" something about encryption, disallowed in France.
|
||||
* 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
|
||||
|
||||
Prerequisites: Android Studio with Java SDK installed
|
||||
Prerequisites: Android Studio with SDK installed
|
||||
|
||||
1. Build the web assets:
|
||||
|
||||
@@ -460,9 +443,7 @@ Prerequisites: Android Studio with Java SDK installed
|
||||
* Then `bundleRelease`:
|
||||
|
||||
```bash
|
||||
cd android
|
||||
./gradlew bundleRelease -Dlint.baselines.continue=true
|
||||
cd -
|
||||
```
|
||||
|
||||
... and find your `aab` file at app/build/outputs/bundle/release
|
||||
@@ -475,8 +456,6 @@ At play.google.com/console:
|
||||
- Hit "Next".
|
||||
- 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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
### Added
|
||||
- Total amounts of gives on project page
|
||||
|
||||
@@ -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**
|
||||
@@ -31,8 +31,8 @@ android {
|
||||
applicationId "app.timesafari.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 26
|
||||
versionName "0.5.1"
|
||||
versionCode 18
|
||||
versionName "0.4.7"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// 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.core:core-splashscreen:$coreSplashScreenVersion"
|
||||
implementation project(':capacitor-android')
|
||||
implementation project(':capacitor-community-sqlite')
|
||||
implementation "androidx.biometric:biometric:1.2.0-alpha05"
|
||||
testImplementation "junit:junit:$junitVersion"
|
||||
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
||||
|
||||
@@ -9,7 +9,6 @@ android {
|
||||
|
||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||
dependencies {
|
||||
implementation project(':capacitor-community-sqlite')
|
||||
implementation project(':capacitor-mlkit-barcode-scanning')
|
||||
implementation project(':capacitor-app')
|
||||
implementation project(':capacitor-camera')
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
[
|
||||
{
|
||||
"pkg": "@capacitor-community/sqlite",
|
||||
"classpath": "com.getcapacitor.community.database.sqlite.CapacitorSQLitePlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capacitor-mlkit/barcode-scanning",
|
||||
"classpath": "io.capawesome.capacitorjs.plugins.mlkit.barcodescanning.BarcodeScannerPlugin"
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
package app.timesafari;
|
||||
|
||||
import android.os.Bundle;
|
||||
import com.getcapacitor.BridgeActivity;
|
||||
//import com.getcapacitor.community.sqlite.SQLite;
|
||||
|
||||
public class MainActivity extends BridgeActivity {
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Initialize SQLite
|
||||
//registerPlugin(SQLite.class);
|
||||
}
|
||||
// ... existing code ...
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package timesafari.app;
|
||||
|
||||
import com.getcapacitor.BridgeActivity;
|
||||
|
||||
public class MainActivity extends BridgeActivity {}
|
||||
BIN
android/app/src/main/res/drawable-land-hdpi/splash.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
android/app/src/main/res/drawable-land-mdpi/splash.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
android/app/src/main/res/drawable-land-xhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
android/app/src/main/res/drawable-land-xxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
android/app/src/main/res/drawable-land-xxxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
android/app/src/main/res/drawable-port-hdpi/splash.png
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
BIN
android/app/src/main/res/drawable-port-mdpi/splash.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
android/app/src/main/res/drawable-port-xhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
android/app/src/main/res/drawable-port-xxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
android/app/src/main/res/drawable-port-xxxhdpi/splash.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
android/app/src/main/res/drawable/splash.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
@@ -1,9 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background>
|
||||
<inset android:drawable="@mipmap/ic_launcher_background" android:inset="16.7%" />
|
||||
</background>
|
||||
<foreground>
|
||||
<inset android:drawable="@mipmap/ic_launcher_foreground" android:inset="16.7%" />
|
||||
</foreground>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -1,9 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background>
|
||||
<inset android:drawable="@mipmap/ic_launcher_background" android:inset="16.7%" />
|
||||
</background>
|
||||
<foreground>
|
||||
<inset android:drawable="@mipmap/ic_launcher_foreground" android:inset="16.7%" />
|
||||
</foreground>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 15 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
@@ -2,9 +2,6 @@
|
||||
include ':capacitor-android'
|
||||
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'
|
||||
project(':capacitor-mlkit-barcode-scanning').projectDir = new File('../node_modules/@capacitor-mlkit/barcode-scanning/android')
|
||||
|
||||
|
||||
BIN
assets/icon-only.jpg
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
assets/icon.png
|
Before Width: | Height: | Size: 279 KiB |
|
Before Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 1.9 MiB |
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -1,399 +0,0 @@
|
||||
# Dexie to absurd-sql Mapping Guide
|
||||
|
||||
## Schema Mapping
|
||||
|
||||
### Current Dexie Schema
|
||||
```typescript
|
||||
// Current Dexie schema
|
||||
const db = new Dexie('TimeSafariDB');
|
||||
|
||||
db.version(1).stores({
|
||||
accounts: 'did, publicKeyHex, createdAt, updatedAt',
|
||||
settings: 'key, value, updatedAt',
|
||||
contacts: 'id, did, name, createdAt, updatedAt'
|
||||
});
|
||||
```
|
||||
|
||||
### New SQLite Schema
|
||||
```sql
|
||||
-- New SQLite schema
|
||||
CREATE TABLE accounts (
|
||||
did TEXT PRIMARY KEY,
|
||||
public_key_hex TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
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)
|
||||
);
|
||||
|
||||
-- Indexes for performance
|
||||
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);
|
||||
```
|
||||
|
||||
## Query Mapping
|
||||
|
||||
### 1. Account Operations
|
||||
|
||||
#### Get Account by DID
|
||||
```typescript
|
||||
// Dexie
|
||||
const account = await db.accounts.get(did);
|
||||
|
||||
// absurd-sql
|
||||
const result = await db.exec(`
|
||||
SELECT * FROM accounts WHERE did = ?
|
||||
`, [did]);
|
||||
const account = result[0]?.values[0];
|
||||
```
|
||||
|
||||
#### Get All Accounts
|
||||
```typescript
|
||||
// Dexie
|
||||
const accounts = await db.accounts.toArray();
|
||||
|
||||
// absurd-sql
|
||||
const result = await db.exec(`
|
||||
SELECT * FROM accounts ORDER BY created_at DESC
|
||||
`);
|
||||
const accounts = result[0]?.values || [];
|
||||
```
|
||||
|
||||
#### Add Account
|
||||
```typescript
|
||||
// Dexie
|
||||
await db.accounts.add({
|
||||
did,
|
||||
publicKeyHex,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now()
|
||||
});
|
||||
|
||||
// absurd-sql
|
||||
await db.run(`
|
||||
INSERT INTO accounts (did, public_key_hex, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`, [did, publicKeyHex, Date.now(), Date.now()]);
|
||||
```
|
||||
|
||||
#### Update Account
|
||||
```typescript
|
||||
// Dexie
|
||||
await db.accounts.update(did, {
|
||||
publicKeyHex,
|
||||
updatedAt: Date.now()
|
||||
});
|
||||
|
||||
// absurd-sql
|
||||
await db.run(`
|
||||
UPDATE accounts
|
||||
SET public_key_hex = ?, updated_at = ?
|
||||
WHERE did = ?
|
||||
`, [publicKeyHex, Date.now(), did]);
|
||||
```
|
||||
|
||||
### 2. Settings Operations
|
||||
|
||||
#### Get Setting
|
||||
```typescript
|
||||
// Dexie
|
||||
const setting = await db.settings.get(key);
|
||||
|
||||
// absurd-sql
|
||||
const result = await db.exec(`
|
||||
SELECT * FROM settings WHERE key = ?
|
||||
`, [key]);
|
||||
const setting = result[0]?.values[0];
|
||||
```
|
||||
|
||||
#### Set Setting
|
||||
```typescript
|
||||
// Dexie
|
||||
await db.settings.put({
|
||||
key,
|
||||
value,
|
||||
updatedAt: Date.now()
|
||||
});
|
||||
|
||||
// absurd-sql
|
||||
await db.run(`
|
||||
INSERT INTO settings (key, value, updated_at)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET
|
||||
value = excluded.value,
|
||||
updated_at = excluded.updated_at
|
||||
`, [key, value, Date.now()]);
|
||||
```
|
||||
|
||||
### 3. Contact Operations
|
||||
|
||||
#### Get Contacts by Account
|
||||
```typescript
|
||||
// Dexie
|
||||
const contacts = await db.contacts
|
||||
.where('did')
|
||||
.equals(accountDid)
|
||||
.toArray();
|
||||
|
||||
// absurd-sql
|
||||
const result = await db.exec(`
|
||||
SELECT * FROM contacts
|
||||
WHERE did = ?
|
||||
ORDER BY created_at DESC
|
||||
`, [accountDid]);
|
||||
const contacts = result[0]?.values || [];
|
||||
```
|
||||
|
||||
#### Add Contact
|
||||
```typescript
|
||||
// Dexie
|
||||
await db.contacts.add({
|
||||
id: generateId(),
|
||||
did: accountDid,
|
||||
name,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now()
|
||||
});
|
||||
|
||||
// absurd-sql
|
||||
await db.run(`
|
||||
INSERT INTO contacts (id, did, name, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`, [generateId(), accountDid, name, Date.now(), Date.now()]);
|
||||
```
|
||||
|
||||
## Transaction Mapping
|
||||
|
||||
### Batch Operations
|
||||
```typescript
|
||||
// Dexie
|
||||
await db.transaction('rw', [db.accounts, db.contacts], async () => {
|
||||
await db.accounts.add(account);
|
||||
await db.contacts.bulkAdd(contacts);
|
||||
});
|
||||
|
||||
// absurd-sql
|
||||
await db.exec('BEGIN TRANSACTION;');
|
||||
try {
|
||||
await db.run(`
|
||||
INSERT INTO accounts (did, public_key_hex, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`, [account.did, account.publicKeyHex, account.createdAt, account.updatedAt]);
|
||||
|
||||
for (const contact of contacts) {
|
||||
await db.run(`
|
||||
INSERT INTO contacts (id, did, name, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`, [contact.id, contact.did, contact.name, contact.createdAt, contact.updatedAt]);
|
||||
}
|
||||
await db.exec('COMMIT;');
|
||||
} catch (error) {
|
||||
await db.exec('ROLLBACK;');
|
||||
throw error;
|
||||
}
|
||||
```
|
||||
|
||||
## Migration Helper Functions
|
||||
|
||||
### 1. Data Export (Dexie to JSON)
|
||||
```typescript
|
||||
async function exportDexieData(): Promise<MigrationData> {
|
||||
const db = new Dexie('TimeSafariDB');
|
||||
|
||||
return {
|
||||
accounts: await db.accounts.toArray(),
|
||||
settings: await db.settings.toArray(),
|
||||
contacts: await db.contacts.toArray(),
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
timestamp: Date.now(),
|
||||
dexieVersion: Dexie.version
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Data Import (JSON to absurd-sql)
|
||||
```typescript
|
||||
async function importToAbsurdSql(data: MigrationData): Promise<void> {
|
||||
await db.exec('BEGIN TRANSACTION;');
|
||||
try {
|
||||
// Import accounts
|
||||
for (const account of data.accounts) {
|
||||
await db.run(`
|
||||
INSERT INTO accounts (did, public_key_hex, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`, [account.did, account.publicKeyHex, account.createdAt, account.updatedAt]);
|
||||
}
|
||||
|
||||
// Import settings
|
||||
for (const setting of data.settings) {
|
||||
await db.run(`
|
||||
INSERT INTO settings (key, value, updated_at)
|
||||
VALUES (?, ?, ?)
|
||||
`, [setting.key, setting.value, setting.updatedAt]);
|
||||
}
|
||||
|
||||
// Import contacts
|
||||
for (const contact of data.contacts) {
|
||||
await db.run(`
|
||||
INSERT INTO contacts (id, did, name, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`, [contact.id, contact.did, contact.name, contact.createdAt, contact.updatedAt]);
|
||||
}
|
||||
await db.exec('COMMIT;');
|
||||
} catch (error) {
|
||||
await db.exec('ROLLBACK;');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Verification
|
||||
```typescript
|
||||
async function verifyMigration(dexieData: MigrationData): Promise<boolean> {
|
||||
// Verify account count
|
||||
const accountResult = await db.exec('SELECT COUNT(*) as count FROM accounts');
|
||||
const accountCount = accountResult[0].values[0][0];
|
||||
if (accountCount !== dexieData.accounts.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify settings count
|
||||
const settingsResult = await db.exec('SELECT COUNT(*) as count FROM settings');
|
||||
const settingsCount = settingsResult[0].values[0][0];
|
||||
if (settingsCount !== dexieData.settings.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify contacts count
|
||||
const contactsResult = await db.exec('SELECT COUNT(*) as count FROM contacts');
|
||||
const contactsCount = contactsResult[0].values[0][0];
|
||||
if (contactsCount !== dexieData.contacts.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify data integrity
|
||||
for (const account of dexieData.accounts) {
|
||||
const result = await db.exec(
|
||||
'SELECT * FROM accounts WHERE did = ?',
|
||||
[account.did]
|
||||
);
|
||||
const migratedAccount = result[0]?.values[0];
|
||||
if (!migratedAccount ||
|
||||
migratedAccount[1] !== account.publicKeyHex) { // public_key_hex is second column
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### 1. Indexing
|
||||
- Dexie automatically creates indexes based on the schema
|
||||
- absurd-sql requires explicit index creation
|
||||
- Added indexes for frequently queried fields
|
||||
- Use `PRAGMA journal_mode=MEMORY;` for better performance
|
||||
|
||||
### 2. Batch Operations
|
||||
- Dexie has built-in bulk operations
|
||||
- absurd-sql uses transactions for batch operations
|
||||
- Consider chunking large datasets
|
||||
- Use prepared statements for repeated queries
|
||||
|
||||
### 3. Query Optimization
|
||||
- Dexie uses IndexedDB's native indexing
|
||||
- absurd-sql requires explicit query optimization
|
||||
- Use prepared statements for repeated queries
|
||||
- Consider using `PRAGMA synchronous=NORMAL;` for better performance
|
||||
|
||||
## Error Handling
|
||||
|
||||
### 1. Common Errors
|
||||
```typescript
|
||||
// Dexie errors
|
||||
try {
|
||||
await db.accounts.add(account);
|
||||
} catch (error) {
|
||||
if (error instanceof Dexie.ConstraintError) {
|
||||
// Handle duplicate key
|
||||
}
|
||||
}
|
||||
|
||||
// absurd-sql errors
|
||||
try {
|
||||
await db.run(`
|
||||
INSERT INTO accounts (did, public_key_hex, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`, [account.did, account.publicKeyHex, account.createdAt, account.updatedAt]);
|
||||
} catch (error) {
|
||||
if (error.message.includes('UNIQUE constraint failed')) {
|
||||
// Handle duplicate key
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Transaction Recovery
|
||||
```typescript
|
||||
// Dexie transaction
|
||||
try {
|
||||
await db.transaction('rw', db.accounts, async () => {
|
||||
// Operations
|
||||
});
|
||||
} catch (error) {
|
||||
// Dexie automatically rolls back
|
||||
}
|
||||
|
||||
// absurd-sql transaction
|
||||
try {
|
||||
await db.exec('BEGIN TRANSACTION;');
|
||||
// Operations
|
||||
await db.exec('COMMIT;');
|
||||
} catch (error) {
|
||||
await db.exec('ROLLBACK;');
|
||||
throw error;
|
||||
}
|
||||
```
|
||||
|
||||
## Migration Strategy
|
||||
|
||||
1. **Preparation**
|
||||
- Export all Dexie data
|
||||
- Verify data integrity
|
||||
- Create SQLite schema
|
||||
- Setup indexes
|
||||
|
||||
2. **Migration**
|
||||
- Import data in transactions
|
||||
- Verify each batch
|
||||
- Handle errors gracefully
|
||||
- Maintain backup
|
||||
|
||||
3. **Verification**
|
||||
- Compare record counts
|
||||
- Verify data integrity
|
||||
- Test common queries
|
||||
- Validate relationships
|
||||
|
||||
4. **Cleanup**
|
||||
- Remove Dexie database
|
||||
- Clear IndexedDB storage
|
||||
- Update application code
|
||||
- Remove old dependencies
|
||||
@@ -1,613 +0,0 @@
|
||||
# Migration Guide: Dexie to absurd-sql
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the migration process from Dexie.js to absurd-sql for the TimeSafari app's storage implementation. The migration aims to provide a consistent SQLite-based storage solution across all platforms while maintaining data integrity and ensuring a smooth transition for users.
|
||||
|
||||
## Migration Goals
|
||||
|
||||
1. **Data Integrity**
|
||||
- Preserve all existing data
|
||||
- Maintain data relationships
|
||||
- Ensure data consistency
|
||||
|
||||
2. **Performance**
|
||||
- Improve query performance
|
||||
- Reduce storage overhead
|
||||
- Optimize for platform-specific features
|
||||
|
||||
3. **Security**
|
||||
- Maintain or improve encryption
|
||||
- Preserve access controls
|
||||
- Enhance data protection
|
||||
|
||||
4. **User Experience**
|
||||
- Zero data loss
|
||||
- Minimal downtime
|
||||
- Automatic migration where possible
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **Backup Requirements**
|
||||
```typescript
|
||||
interface MigrationBackup {
|
||||
timestamp: number;
|
||||
accounts: Account[];
|
||||
settings: Setting[];
|
||||
contacts: Contact[];
|
||||
metadata: {
|
||||
version: string;
|
||||
platform: string;
|
||||
dexieVersion: string;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
2. **Dependencies**
|
||||
```json
|
||||
{
|
||||
"@jlongster/sql.js": "^1.8.0",
|
||||
"absurd-sql": "^1.8.0"
|
||||
}
|
||||
```
|
||||
|
||||
3. **Storage Requirements**
|
||||
- Sufficient IndexedDB quota
|
||||
- Available disk space for SQLite
|
||||
- Backup storage space
|
||||
|
||||
4. **Platform Support**
|
||||
- Web: Modern browser with IndexedDB support
|
||||
- iOS: iOS 13+ with SQLite support
|
||||
- Android: Android 5+ with SQLite support
|
||||
- Electron: Latest version with SQLite support
|
||||
|
||||
## Migration Process
|
||||
|
||||
### 1. Preparation
|
||||
|
||||
```typescript
|
||||
// src/services/storage/migration/MigrationService.ts
|
||||
import initSqlJs from '@jlongster/sql.js';
|
||||
import { SQLiteFS } from 'absurd-sql';
|
||||
import IndexedDBBackend from 'absurd-sql/dist/indexeddb-backend';
|
||||
|
||||
export class MigrationService {
|
||||
private static instance: MigrationService;
|
||||
private backup: MigrationBackup | null = null;
|
||||
private sql: any = null;
|
||||
private db: any = null;
|
||||
|
||||
async prepare(): Promise<void> {
|
||||
try {
|
||||
// 1. Check prerequisites
|
||||
await this.checkPrerequisites();
|
||||
|
||||
// 2. Create backup
|
||||
this.backup = await this.createBackup();
|
||||
|
||||
// 3. Verify backup integrity
|
||||
await this.verifyBackup();
|
||||
|
||||
// 4. Initialize absurd-sql
|
||||
await this.initializeAbsurdSql();
|
||||
} catch (error) {
|
||||
throw new StorageError(
|
||||
'Migration preparation failed',
|
||||
StorageErrorCodes.MIGRATION_FAILED,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async initializeAbsurdSql(): Promise<void> {
|
||||
// Initialize SQL.js
|
||||
this.sql = await initSqlJs({
|
||||
locateFile: (file: string) => {
|
||||
return new URL(`/node_modules/@jlongster/sql.js/dist/${file}`, import.meta.url).href;
|
||||
}
|
||||
});
|
||||
|
||||
// Setup SQLiteFS with IndexedDB backend
|
||||
const sqlFS = new SQLiteFS(this.sql.FS, new IndexedDBBackend());
|
||||
this.sql.register_for_idb(sqlFS);
|
||||
|
||||
// Create and mount filesystem
|
||||
this.sql.FS.mkdir('/sql');
|
||||
this.sql.FS.mount(sqlFS, {}, '/sql');
|
||||
|
||||
// Open database
|
||||
const path = '/sql/db.sqlite';
|
||||
if (typeof SharedArrayBuffer === 'undefined') {
|
||||
let stream = this.sql.FS.open(path, 'a+');
|
||||
await stream.node.contents.readIfFallback();
|
||||
this.sql.FS.close(stream);
|
||||
}
|
||||
|
||||
this.db = new this.sql.Database(path, { filename: true });
|
||||
if (!this.db) {
|
||||
throw new StorageError(
|
||||
'Database initialization failed',
|
||||
StorageErrorCodes.INITIALIZATION_FAILED
|
||||
);
|
||||
}
|
||||
|
||||
// Configure database
|
||||
await this.db.exec(`PRAGMA journal_mode=MEMORY;`);
|
||||
}
|
||||
|
||||
private async checkPrerequisites(): Promise<void> {
|
||||
// Check IndexedDB availability
|
||||
if (!window.indexedDB) {
|
||||
throw new StorageError(
|
||||
'IndexedDB not available',
|
||||
StorageErrorCodes.INITIALIZATION_FAILED
|
||||
);
|
||||
}
|
||||
|
||||
// Check storage quota
|
||||
const quota = await navigator.storage.estimate();
|
||||
if (quota.quota && quota.usage && quota.usage > quota.quota * 0.9) {
|
||||
throw new StorageError(
|
||||
'Insufficient storage space',
|
||||
StorageErrorCodes.STORAGE_FULL
|
||||
);
|
||||
}
|
||||
|
||||
// Check platform support
|
||||
const capabilities = await PlatformDetection.getCapabilities();
|
||||
if (!capabilities.hasFileSystem) {
|
||||
throw new StorageError(
|
||||
'Platform does not support required features',
|
||||
StorageErrorCodes.INITIALIZATION_FAILED
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async createBackup(): Promise<MigrationBackup> {
|
||||
const dexieDB = new Dexie('TimeSafariDB');
|
||||
|
||||
return {
|
||||
timestamp: Date.now(),
|
||||
accounts: await dexieDB.accounts.toArray(),
|
||||
settings: await dexieDB.settings.toArray(),
|
||||
contacts: await dexieDB.contacts.toArray(),
|
||||
metadata: {
|
||||
version: '1.0.0',
|
||||
platform: await PlatformDetection.getPlatform(),
|
||||
dexieVersion: Dexie.version
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Data Migration
|
||||
|
||||
```typescript
|
||||
// src/services/storage/migration/DataMigration.ts
|
||||
export class DataMigration {
|
||||
async migrate(backup: MigrationBackup): Promise<void> {
|
||||
try {
|
||||
// 1. Create new database schema
|
||||
await this.createSchema();
|
||||
|
||||
// 2. Migrate accounts
|
||||
await this.migrateAccounts(backup.accounts);
|
||||
|
||||
// 3. Migrate settings
|
||||
await this.migrateSettings(backup.settings);
|
||||
|
||||
// 4. Migrate contacts
|
||||
await this.migrateContacts(backup.contacts);
|
||||
|
||||
// 5. Verify migration
|
||||
await this.verifyMigration(backup);
|
||||
} catch (error) {
|
||||
// 6. Handle failure
|
||||
await this.handleMigrationFailure(error, backup);
|
||||
}
|
||||
}
|
||||
|
||||
private async migrateAccounts(accounts: Account[]): Promise<void> {
|
||||
// Use transaction for atomicity
|
||||
await this.db.exec('BEGIN TRANSACTION;');
|
||||
try {
|
||||
for (const account of accounts) {
|
||||
await this.db.run(`
|
||||
INSERT INTO accounts (did, public_key_hex, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`, [
|
||||
account.did,
|
||||
account.publicKeyHex,
|
||||
account.createdAt,
|
||||
account.updatedAt
|
||||
]);
|
||||
}
|
||||
await this.db.exec('COMMIT;');
|
||||
} catch (error) {
|
||||
await this.db.exec('ROLLBACK;');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async verifyMigration(backup: MigrationBackup): Promise<void> {
|
||||
// Verify account count
|
||||
const result = await this.db.exec('SELECT COUNT(*) as count FROM accounts');
|
||||
const accountCount = result[0].values[0][0];
|
||||
|
||||
if (accountCount !== backup.accounts.length) {
|
||||
throw new StorageError(
|
||||
'Account count mismatch',
|
||||
StorageErrorCodes.VERIFICATION_FAILED
|
||||
);
|
||||
}
|
||||
|
||||
// Verify data integrity
|
||||
await this.verifyDataIntegrity(backup);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Rollback Strategy
|
||||
|
||||
```typescript
|
||||
// src/services/storage/migration/RollbackService.ts
|
||||
export class RollbackService {
|
||||
async rollback(backup: MigrationBackup): Promise<void> {
|
||||
try {
|
||||
// 1. Stop all database operations
|
||||
await this.stopDatabaseOperations();
|
||||
|
||||
// 2. Restore from backup
|
||||
await this.restoreFromBackup(backup);
|
||||
|
||||
// 3. Verify restoration
|
||||
await this.verifyRestoration(backup);
|
||||
|
||||
// 4. Clean up absurd-sql
|
||||
await this.cleanupAbsurdSql();
|
||||
} catch (error) {
|
||||
throw new StorageError(
|
||||
'Rollback failed',
|
||||
StorageErrorCodes.ROLLBACK_FAILED,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async restoreFromBackup(backup: MigrationBackup): Promise<void> {
|
||||
const dexieDB = new Dexie('TimeSafariDB');
|
||||
|
||||
// Restore accounts
|
||||
await dexieDB.accounts.bulkPut(backup.accounts);
|
||||
|
||||
// Restore settings
|
||||
await dexieDB.settings.bulkPut(backup.settings);
|
||||
|
||||
// Restore contacts
|
||||
await dexieDB.contacts.bulkPut(backup.contacts);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Migration UI
|
||||
|
||||
```vue
|
||||
<!-- src/components/MigrationProgress.vue -->
|
||||
<template>
|
||||
<div class="migration-progress">
|
||||
<h2>Database Migration</h2>
|
||||
|
||||
<div class="progress-container">
|
||||
<div class="progress-bar" :style="{ width: `${progress}%` }" />
|
||||
<div class="progress-text">{{ progress }}%</div>
|
||||
</div>
|
||||
|
||||
<div class="status-message">{{ statusMessage }}</div>
|
||||
|
||||
<div v-if="error" class="error-message">
|
||||
{{ error }}
|
||||
<button @click="retryMigration">Retry</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { MigrationService } from '@/services/storage/migration/MigrationService';
|
||||
|
||||
const progress = ref(0);
|
||||
const statusMessage = ref('Preparing migration...');
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
const migrationService = MigrationService.getInstance();
|
||||
|
||||
async function startMigration() {
|
||||
try {
|
||||
// 1. Preparation
|
||||
statusMessage.value = 'Creating backup...';
|
||||
await migrationService.prepare();
|
||||
progress.value = 20;
|
||||
|
||||
// 2. Data migration
|
||||
statusMessage.value = 'Migrating data...';
|
||||
await migrationService.migrate();
|
||||
progress.value = 80;
|
||||
|
||||
// 3. Verification
|
||||
statusMessage.value = 'Verifying migration...';
|
||||
await migrationService.verify();
|
||||
progress.value = 100;
|
||||
|
||||
statusMessage.value = 'Migration completed successfully!';
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err.message : 'Migration failed';
|
||||
statusMessage.value = 'Migration failed';
|
||||
}
|
||||
}
|
||||
|
||||
async function retryMigration() {
|
||||
error.value = null;
|
||||
progress.value = 0;
|
||||
await startMigration();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
startMigration();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.migration-progress {
|
||||
padding: 2rem;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
position: relative;
|
||||
height: 20px;
|
||||
background: #eee;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
background: #4CAF50;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
line-height: 20px;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.status-message {
|
||||
text-align: center;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #f44336;
|
||||
text-align: center;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 1rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #1976D2;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
1. **Unit Tests**
|
||||
```typescript
|
||||
// src/services/storage/migration/__tests__/MigrationService.spec.ts
|
||||
describe('MigrationService', () => {
|
||||
it('should initialize absurd-sql correctly', async () => {
|
||||
const service = MigrationService.getInstance();
|
||||
await service.initializeAbsurdSql();
|
||||
|
||||
expect(service.isInitialized()).toBe(true);
|
||||
expect(service.getDatabase()).toBeDefined();
|
||||
});
|
||||
|
||||
it('should create valid backup', async () => {
|
||||
const service = MigrationService.getInstance();
|
||||
const backup = await service.createBackup();
|
||||
|
||||
expect(backup).toBeDefined();
|
||||
expect(backup.accounts).toBeInstanceOf(Array);
|
||||
expect(backup.settings).toBeInstanceOf(Array);
|
||||
expect(backup.contacts).toBeInstanceOf(Array);
|
||||
});
|
||||
|
||||
it('should migrate data correctly', async () => {
|
||||
const service = MigrationService.getInstance();
|
||||
const backup = await service.createBackup();
|
||||
|
||||
await service.migrate(backup);
|
||||
|
||||
// Verify migration
|
||||
const accounts = await service.getMigratedAccounts();
|
||||
expect(accounts).toHaveLength(backup.accounts.length);
|
||||
});
|
||||
|
||||
it('should handle rollback correctly', async () => {
|
||||
const service = MigrationService.getInstance();
|
||||
const backup = await service.createBackup();
|
||||
|
||||
// Simulate failed migration
|
||||
await service.migrate(backup);
|
||||
await service.simulateFailure();
|
||||
|
||||
// Perform rollback
|
||||
await service.rollback(backup);
|
||||
|
||||
// Verify rollback
|
||||
const accounts = await service.getOriginalAccounts();
|
||||
expect(accounts).toHaveLength(backup.accounts.length);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
2. **Integration Tests**
|
||||
```typescript
|
||||
// src/services/storage/migration/__tests__/integration/Migration.spec.ts
|
||||
describe('Migration Integration', () => {
|
||||
it('should handle concurrent access during migration', async () => {
|
||||
const service = MigrationService.getInstance();
|
||||
|
||||
// Start migration
|
||||
const migrationPromise = service.migrate();
|
||||
|
||||
// Simulate concurrent access
|
||||
const accessPromises = Array(5).fill(null).map(() =>
|
||||
service.getAccount('did:test:123')
|
||||
);
|
||||
|
||||
// Wait for all operations
|
||||
const [migrationResult, ...accessResults] = await Promise.allSettled([
|
||||
migrationPromise,
|
||||
...accessPromises
|
||||
]);
|
||||
|
||||
// Verify results
|
||||
expect(migrationResult.status).toBe('fulfilled');
|
||||
expect(accessResults.some(r => r.status === 'rejected')).toBe(true);
|
||||
});
|
||||
|
||||
it('should maintain data integrity during platform transition', async () => {
|
||||
const service = MigrationService.getInstance();
|
||||
|
||||
// Simulate platform change
|
||||
await service.simulatePlatformChange();
|
||||
|
||||
// Verify data
|
||||
const accounts = await service.getAllAccounts();
|
||||
const settings = await service.getAllSettings();
|
||||
const contacts = await service.getAllContacts();
|
||||
|
||||
expect(accounts).toBeDefined();
|
||||
expect(settings).toBeDefined();
|
||||
expect(contacts).toBeDefined();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. **Data Integrity**
|
||||
- [ ] All accounts migrated successfully
|
||||
- [ ] All settings preserved
|
||||
- [ ] All contacts transferred
|
||||
- [ ] No data corruption
|
||||
|
||||
2. **Performance**
|
||||
- [ ] Migration completes within acceptable time
|
||||
- [ ] No significant performance degradation
|
||||
- [ ] Efficient storage usage
|
||||
- [ ] Smooth user experience
|
||||
|
||||
3. **Security**
|
||||
- [ ] Encrypted data remains secure
|
||||
- [ ] Access controls maintained
|
||||
- [ ] No sensitive data exposure
|
||||
- [ ] Secure backup process
|
||||
|
||||
4. **User Experience**
|
||||
- [ ] Clear migration progress
|
||||
- [ ] Informative error messages
|
||||
- [ ] Automatic recovery from failures
|
||||
- [ ] No data loss
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
1. **Automatic Rollback**
|
||||
- Triggered by migration failure
|
||||
- Restores from verified backup
|
||||
- Maintains data consistency
|
||||
- Logs rollback reason
|
||||
|
||||
2. **Manual Rollback**
|
||||
- Available through settings
|
||||
- Requires user confirmation
|
||||
- Preserves backup data
|
||||
- Provides rollback status
|
||||
|
||||
3. **Emergency Recovery**
|
||||
- Manual backup restoration
|
||||
- Database repair tools
|
||||
- Data recovery procedures
|
||||
- Support contact information
|
||||
|
||||
## Post-Migration
|
||||
|
||||
1. **Verification**
|
||||
- Data integrity checks
|
||||
- Performance monitoring
|
||||
- Error rate tracking
|
||||
- User feedback collection
|
||||
|
||||
2. **Cleanup**
|
||||
- Remove old database
|
||||
- Clear migration artifacts
|
||||
- Update application state
|
||||
- Archive backup data
|
||||
|
||||
3. **Monitoring**
|
||||
- Track migration success rate
|
||||
- Monitor performance metrics
|
||||
- Collect error reports
|
||||
- Gather user feedback
|
||||
|
||||
## Support
|
||||
|
||||
For assistance with migration:
|
||||
1. Check the troubleshooting guide
|
||||
2. Review error logs
|
||||
3. Contact support team
|
||||
4. Submit issue report
|
||||
|
||||
## Timeline
|
||||
|
||||
1. **Preparation Phase** (1 week)
|
||||
- Backup system implementation
|
||||
- Migration service development
|
||||
- Testing framework setup
|
||||
|
||||
2. **Testing Phase** (2 weeks)
|
||||
- Unit testing
|
||||
- Integration testing
|
||||
- Performance testing
|
||||
- Security testing
|
||||
|
||||
3. **Deployment Phase** (1 week)
|
||||
- Staged rollout
|
||||
- Monitoring
|
||||
- Support preparation
|
||||
- Documentation updates
|
||||
|
||||
4. **Post-Deployment** (2 weeks)
|
||||
- Monitoring
|
||||
- Bug fixes
|
||||
- Performance optimization
|
||||
- User feedback collection
|
||||
@@ -1,339 +0,0 @@
|
||||
# Secure Storage Implementation Guide for TimeSafari App
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the implementation of secure storage for the TimeSafari app. The implementation focuses on:
|
||||
|
||||
1. **Platform-Specific Storage Solutions**:
|
||||
- Web: SQLite with IndexedDB backend (absurd-sql)
|
||||
- Electron: SQLite with Node.js backend
|
||||
- Native: (Planned) SQLCipher with platform-specific secure storage
|
||||
|
||||
2. **Key Features**:
|
||||
- SQLite-based storage using absurd-sql for web
|
||||
- Platform-specific service factory pattern
|
||||
- Consistent API across platforms
|
||||
- Migration support from Dexie.js
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Installation
|
||||
|
||||
```bash
|
||||
# Core dependencies
|
||||
npm install @jlongster/sql.js
|
||||
npm install absurd-sql
|
||||
|
||||
# Platform-specific dependencies (for future native support)
|
||||
npm install @capacitor/preferences
|
||||
npm install @capacitor-community/biometric-auth
|
||||
```
|
||||
|
||||
### 2. Basic Usage
|
||||
|
||||
```typescript
|
||||
// Using the platform service
|
||||
import { PlatformServiceFactory } from '../services/PlatformServiceFactory';
|
||||
|
||||
// Get platform-specific service instance
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
|
||||
// Example database operations
|
||||
async function example() {
|
||||
try {
|
||||
// Query example
|
||||
const result = await platformService.dbQuery(
|
||||
"SELECT * FROM accounts WHERE did = ?",
|
||||
[did]
|
||||
);
|
||||
|
||||
// Execute example
|
||||
await platformService.dbExec(
|
||||
"INSERT INTO accounts (did, public_key_hex) VALUES (?, ?)",
|
||||
[did, publicKeyHex]
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Database operation failed:', error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Platform Detection
|
||||
|
||||
```typescript
|
||||
// src/services/PlatformServiceFactory.ts
|
||||
export class PlatformServiceFactory {
|
||||
static getInstance(): PlatformService {
|
||||
if (process.env.ELECTRON) {
|
||||
// Electron platform
|
||||
return new ElectronPlatformService();
|
||||
} else {
|
||||
// Web platform (default)
|
||||
return new AbsurdSqlDatabaseService();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Current Implementation Details
|
||||
|
||||
#### Web Platform (AbsurdSqlDatabaseService)
|
||||
|
||||
The web platform uses absurd-sql with IndexedDB backend:
|
||||
|
||||
```typescript
|
||||
// src/services/AbsurdSqlDatabaseService.ts
|
||||
export class AbsurdSqlDatabaseService implements PlatformService {
|
||||
private static instance: AbsurdSqlDatabaseService | null = null;
|
||||
private db: AbsurdSqlDatabase | null = null;
|
||||
private initialized: boolean = false;
|
||||
|
||||
// Singleton pattern
|
||||
static getInstance(): AbsurdSqlDatabaseService {
|
||||
if (!AbsurdSqlDatabaseService.instance) {
|
||||
AbsurdSqlDatabaseService.instance = new AbsurdSqlDatabaseService();
|
||||
}
|
||||
return AbsurdSqlDatabaseService.instance;
|
||||
}
|
||||
|
||||
// Database operations
|
||||
async dbQuery(sql: string, params: unknown[] = []): Promise<QueryExecResult[]> {
|
||||
await this.waitForInitialization();
|
||||
return this.queueOperation<QueryExecResult[]>("query", sql, params);
|
||||
}
|
||||
|
||||
async dbExec(sql: string, params: unknown[] = []): Promise<void> {
|
||||
await this.waitForInitialization();
|
||||
await this.queueOperation<void>("run", sql, params);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Key features:
|
||||
- Uses absurd-sql for SQLite in the browser
|
||||
- Implements operation queuing for thread safety
|
||||
- Handles initialization and connection management
|
||||
- Provides consistent API across platforms
|
||||
|
||||
### 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
|
||||
|
||||
1. **Functionality**
|
||||
- [x] Basic CRUD operations work correctly
|
||||
- [x] Platform service factory pattern implemented
|
||||
- [x] Error handling in place
|
||||
- [ ] Native platform support (planned)
|
||||
|
||||
2. **Performance**
|
||||
- [x] Database operations complete within acceptable time
|
||||
- [x] Operation queuing for thread safety
|
||||
- [x] Proper initialization handling
|
||||
- [ ] Performance monitoring (planned)
|
||||
|
||||
3. **Security**
|
||||
- [x] Basic data integrity
|
||||
- [ ] Encryption (planned for native platforms)
|
||||
- [ ] Secure key storage (planned)
|
||||
- [ ] Platform-specific security features (planned)
|
||||
|
||||
4. **Testing**
|
||||
- [x] Basic unit tests
|
||||
- [ ] Comprehensive integration tests (planned)
|
||||
- [ ] Platform-specific tests (planned)
|
||||
- [ ] Migration tests (planned)
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Native Platform Support**
|
||||
- Implement SQLCipher for iOS/Android
|
||||
- Add platform-specific secure storage
|
||||
- Implement biometric authentication
|
||||
|
||||
2. **Enhanced Security**
|
||||
- Add encryption for sensitive data
|
||||
- Implement secure key storage
|
||||
- Add platform-specific security features
|
||||
|
||||
3. **Testing and Monitoring**
|
||||
- Add comprehensive test coverage
|
||||
- Implement performance monitoring
|
||||
- Add error tracking and analytics
|
||||
|
||||
4. **Documentation**
|
||||
- Add API documentation
|
||||
- Create migration guides
|
||||
- Document security measures
|
||||
@@ -1,329 +0,0 @@
|
||||
# Storage Implementation Checklist
|
||||
|
||||
## Core Services
|
||||
|
||||
### 1. Storage Service Layer
|
||||
- [x] Create base `PlatformService` interface
|
||||
- [x] Define common methods for all platforms
|
||||
- [x] Add platform-specific method signatures
|
||||
- [x] Include error handling types
|
||||
- [x] Add migration support methods
|
||||
|
||||
- [x] Implement platform-specific services
|
||||
- [x] `AbsurdSqlDatabaseService` (web)
|
||||
- [x] Database initialization
|
||||
- [x] VFS setup with IndexedDB backend
|
||||
- [x] Connection management
|
||||
- [x] Operation queuing
|
||||
- [ ] `NativeSQLiteService` (iOS/Android) (planned)
|
||||
- [ ] SQLCipher integration
|
||||
- [ ] Native bridge setup
|
||||
- [ ] File system access
|
||||
- [ ] `ElectronSQLiteService` (planned)
|
||||
- [ ] Node SQLite integration
|
||||
- [ ] IPC communication
|
||||
- [ ] File system access
|
||||
|
||||
### 2. Migration Services
|
||||
- [x] Implement basic migration support
|
||||
- [x] Dual-storage pattern (SQLite + Dexie)
|
||||
- [x] Basic data verification
|
||||
- [ ] Rollback procedures (planned)
|
||||
- [ ] Progress tracking (planned)
|
||||
- [ ] Create `MigrationUI` components (planned)
|
||||
- [ ] Progress indicators
|
||||
- [ ] Error handling
|
||||
- [ ] User notifications
|
||||
- [ ] Manual triggers
|
||||
|
||||
### 3. Security Layer
|
||||
- [x] Basic data integrity
|
||||
- [ ] Implement `EncryptionService` (planned)
|
||||
- [ ] Key management
|
||||
- [ ] Encryption/decryption
|
||||
- [ ] Secure storage
|
||||
- [ ] Add `BiometricService` (planned)
|
||||
- [ ] Platform detection
|
||||
- [ ] Authentication flow
|
||||
- [ ] Fallback mechanisms
|
||||
|
||||
## Platform-Specific Implementation
|
||||
|
||||
### Web Platform
|
||||
- [x] Setup absurd-sql
|
||||
- [x] Install dependencies
|
||||
```json
|
||||
{
|
||||
"@jlongster/sql.js": "^1.8.0",
|
||||
"absurd-sql": "^1.8.0"
|
||||
}
|
||||
```
|
||||
- [x] Configure VFS with IndexedDB backend
|
||||
- [x] Setup worker threads
|
||||
- [x] Implement operation queuing
|
||||
- [x] Configure database pragmas
|
||||
|
||||
```sql
|
||||
PRAGMA journal_mode=MEMORY;
|
||||
PRAGMA synchronous=NORMAL;
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA busy_timeout=5000;
|
||||
```
|
||||
|
||||
- [x] Update build configuration
|
||||
- [x] Modify `vite.config.ts`
|
||||
- [x] Add worker configuration
|
||||
- [x] Update chunk splitting
|
||||
- [x] Configure asset handling
|
||||
|
||||
- [x] Implement IndexedDB backend
|
||||
- [x] Create database service
|
||||
- [x] Add operation queuing
|
||||
- [x] Handle initialization
|
||||
- [x] Implement atomic operations
|
||||
|
||||
### iOS Platform (Planned)
|
||||
- [ ] Setup SQLCipher
|
||||
- [ ] Install pod dependencies
|
||||
- [ ] Configure encryption
|
||||
- [ ] Setup keychain access
|
||||
- [ ] Implement secure storage
|
||||
|
||||
- [ ] Update Capacitor config
|
||||
- [ ] Modify `capacitor.config.ts`
|
||||
- [ ] Add iOS permissions
|
||||
- [ ] Configure backup
|
||||
- [ ] Setup app groups
|
||||
|
||||
### Android 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
|
||||
- [ ] Install dependencies
|
||||
- [ ] Configure IPC
|
||||
- [ ] Setup file system access
|
||||
- [ ] Implement secure storage
|
||||
|
||||
- [ ] Update Electron config
|
||||
- [ ] Modify `electron.config.ts`
|
||||
- [ ] Add security policies
|
||||
- [ ] Configure file access
|
||||
- [ ] Setup auto-updates
|
||||
|
||||
## Data Models and Types
|
||||
|
||||
### 1. Database Schema
|
||||
- [x] Define tables
|
||||
|
||||
```sql
|
||||
-- Accounts table
|
||||
CREATE TABLE accounts (
|
||||
did TEXT PRIMARY KEY,
|
||||
public_key_hex TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
-- Settings table
|
||||
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)
|
||||
);
|
||||
|
||||
-- Indexes for performance
|
||||
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);
|
||||
```
|
||||
|
||||
- [x] Create indexes
|
||||
- [x] Define constraints
|
||||
- [ ] Add triggers (planned)
|
||||
- [ ] Setup migrations (planned)
|
||||
|
||||
### 2. Type Definitions
|
||||
|
||||
- [x] Create interfaces
|
||||
```typescript
|
||||
interface Account {
|
||||
did: string;
|
||||
publicKeyHex: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
interface Setting {
|
||||
key: string;
|
||||
value: string;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
interface Contact {
|
||||
id: string;
|
||||
did: string;
|
||||
name?: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
```
|
||||
|
||||
- [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
|
||||
|
||||
### 1. Unit Tests
|
||||
- [x] Basic service tests
|
||||
- [x] Platform service tests
|
||||
- [x] Database operation tests
|
||||
- [ ] Security service tests (planned)
|
||||
- [ ] Platform detection tests (planned)
|
||||
|
||||
### 2. Integration Tests (Planned)
|
||||
- [ ] Test migrations
|
||||
- [ ] Web platform tests
|
||||
- [ ] iOS platform tests
|
||||
- [ ] Android platform tests
|
||||
- [ ] Electron platform tests
|
||||
|
||||
### 3. E2E Tests (Planned)
|
||||
- [ ] Test workflows
|
||||
- [ ] Account management
|
||||
- [ ] Settings management
|
||||
- [ ] Contact management
|
||||
- [ ] Migration process
|
||||
|
||||
## Documentation
|
||||
|
||||
### 1. Technical Documentation
|
||||
- [x] Update architecture docs
|
||||
- [x] Add API documentation
|
||||
- [ ] Create migration guides (planned)
|
||||
- [ ] Document security measures (planned)
|
||||
|
||||
### 2. User Documentation (Planned)
|
||||
- [ ] Update user guides
|
||||
- [ ] Add troubleshooting guides
|
||||
- [ ] Create FAQ
|
||||
- [ ] Document new features
|
||||
|
||||
## Deployment
|
||||
|
||||
### 1. Build Process
|
||||
- [x] Update build scripts
|
||||
- [x] Add platform-specific builds
|
||||
- [ ] Configure CI/CD (planned)
|
||||
- [ ] Setup automated testing (planned)
|
||||
|
||||
### 2. Release Process (Planned)
|
||||
- [ ] Create release checklist
|
||||
- [ ] Add version management
|
||||
- [ ] Setup rollback procedures
|
||||
- [ ] Configure monitoring
|
||||
|
||||
## Monitoring and Analytics (Planned)
|
||||
|
||||
### 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
|
||||
- [ ] Review encryption
|
||||
- [ ] Check access controls
|
||||
- [ ] Verify data handling
|
||||
- [ ] Audit dependencies
|
||||
|
||||
### 2. Penetration Testing
|
||||
- [ ] Test data access
|
||||
- [ ] Verify encryption
|
||||
- [ ] Check authentication
|
||||
- [ ] Review permissions
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### 1. Performance
|
||||
- [x] Query response time < 100ms
|
||||
- [x] Operation queuing for thread safety
|
||||
- [x] Proper initialization handling
|
||||
- [ ] Migration time < 5s per 1000 records (planned)
|
||||
- [ ] Storage overhead < 10% (planned)
|
||||
- [ ] Memory usage < 50MB (planned)
|
||||
|
||||
### 2. Reliability
|
||||
- [x] Basic data integrity
|
||||
- [x] Operation queuing
|
||||
- [ ] Automatic recovery (planned)
|
||||
- [ ] Backup verification (planned)
|
||||
- [ ] Transaction atomicity (planned)
|
||||
- [ ] Data consistency (planned)
|
||||
|
||||
### 3. Security
|
||||
- [x] Basic data integrity
|
||||
- [ ] AES-256 encryption (planned)
|
||||
- [ ] Secure key storage (planned)
|
||||
- [ ] Access control (planned)
|
||||
- [ ] Audit logging (planned)
|
||||
|
||||
### 4. User Experience
|
||||
- [x] Basic database operations
|
||||
- [ ] Smooth migration (planned)
|
||||
- [ ] Clear error messages (planned)
|
||||
- [ ] Progress indicators (planned)
|
||||
- [ ] Recovery options (planned)
|
||||
2077
docs/secure-storage-implementation.md
Normal file
13
ios/.gitignore
vendored
@@ -11,16 +11,3 @@ capacitor-cordova-ios-plugins
|
||||
# Generated Config files
|
||||
App/App/capacitor.config.json
|
||||
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
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };
|
||||
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
|
||||
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 */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
@@ -27,9 +27,9 @@
|
||||
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>"; };
|
||||
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; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
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>"; };
|
||||
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 */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -37,17 +37,17 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
97EF2DC6FD76C3643D680B8D /* Pods_App.framework in Frameworks */,
|
||||
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
4B546315E668C7A13939F417 /* Frameworks */ = {
|
||||
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
90DCAFB4D8948F7A50C13800 /* Pods_App.framework */,
|
||||
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
@@ -57,8 +57,8 @@
|
||||
children = (
|
||||
504EC3061FED79650016851F /* App */,
|
||||
504EC3051FED79650016851F /* Products */,
|
||||
BA325FFCDCE8D334E5C7AEBE /* Pods */,
|
||||
4B546315E668C7A13939F417 /* Frameworks */,
|
||||
7F8756D8B27F46E3366F6CEA /* Pods */,
|
||||
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -85,13 +85,13 @@
|
||||
path = App;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BA325FFCDCE8D334E5C7AEBE /* Pods */ = {
|
||||
7F8756D8B27F46E3366F6CEA /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */,
|
||||
E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */,
|
||||
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */,
|
||||
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
name = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
@@ -101,13 +101,12 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */;
|
||||
buildPhases = (
|
||||
92977BEA1068CC097A57FC77 /* [CP] Check Pods Manifest.lock */,
|
||||
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */,
|
||||
504EC3001FED79650016851F /* Sources */,
|
||||
504EC3011FED79650016851F /* Frameworks */,
|
||||
504EC3021FED79650016851F /* Resources */,
|
||||
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */,
|
||||
012076E8FFE4BF260A79B034 /* Fix Privacy Manifest */,
|
||||
3525031ED1C96EF4CF6E9959 /* [CP] Embed Pods Frameworks */,
|
||||
96A7EF592DF3366D00084D51 /* Fix Privacy Manifest */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -187,10 +186,28 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PROJECT_DIR}/app_privacy_manifest_fixer/fixer.sh\" \n";
|
||||
shellScript = "\"${PROJECT_DIR}/app_privacy_manifest_fixer/fixer.sh\" ";
|
||||
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;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
@@ -205,47 +222,6 @@
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-frameworks.sh\"\n";
|
||||
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 */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@@ -399,12 +375,11 @@
|
||||
};
|
||||
504EC3171FED79650016851F /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */;
|
||||
baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 26;
|
||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||
CURRENT_PROJECT_VERSION = 18;
|
||||
ENABLE_APP_SANDBOX = NO;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
@@ -413,7 +388,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.5.1;
|
||||
MARKETING_VERSION = 0.4.7;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -426,12 +401,11 @@
|
||||
};
|
||||
504EC3181FED79650016851F /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */;
|
||||
baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 26;
|
||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||
CURRENT_PROJECT_VERSION = 18;
|
||||
ENABLE_APP_SANDBOX = NO;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
@@ -440,7 +414,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.5.1;
|
||||
MARKETING_VERSION = 0.4.7;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import UIKit
|
||||
import Capacitor
|
||||
import CapacitorCommunitySqlite
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
@@ -8,10 +7,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
var window: UIWindow?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
// Initialize SQLite
|
||||
//let sqlite = SQLite()
|
||||
//sqlite.initialize()
|
||||
|
||||
// Override point for customization after application launch.
|
||||
return true
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 116 KiB |
14
ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"idiom": "universal",
|
||||
"size": "1024x1024",
|
||||
"filename": "AppIcon-512@2x.png",
|
||||
"platform": "ios"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
23
ios/App/App/Assets.xcassets/Splash.imageset/Contents.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
BIN
ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-1.png
vendored
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-2.png
vendored
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732.png
vendored
Normal file
|
After Width: | Height: | Size: 40 KiB |
@@ -11,7 +11,6 @@ install! 'cocoapods', :disable_input_output_paths => true
|
||||
def capacitor_pods
|
||||
pod 'Capacitor', :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 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
||||
pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera'
|
||||
@@ -27,9 +26,4 @@ end
|
||||
|
||||
post_install do |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
|
||||
|
||||
@@ -5,10 +5,6 @@ PODS:
|
||||
- Capacitor
|
||||
- CapacitorCamera (6.1.2):
|
||||
- Capacitor
|
||||
- CapacitorCommunitySqlite (6.0.2):
|
||||
- Capacitor
|
||||
- SQLCipher
|
||||
- ZIPFoundation
|
||||
- CapacitorCordova (6.2.1)
|
||||
- CapacitorFilesystem (6.0.3):
|
||||
- Capacitor
|
||||
@@ -77,18 +73,11 @@ PODS:
|
||||
- nanopb/decode (2.30910.0)
|
||||
- nanopb/encode (2.30910.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:
|
||||
- "Capacitor (from `../../node_modules/@capacitor/ios`)"
|
||||
- "CapacitorApp (from `../../node_modules/@capacitor/app`)"
|
||||
- "CapacitorCamera (from `../../node_modules/@capacitor/camera`)"
|
||||
- "CapacitorCommunitySqlite (from `../../node_modules/@capacitor-community/sqlite`)"
|
||||
- "CapacitorCordova (from `../../node_modules/@capacitor/ios`)"
|
||||
- "CapacitorFilesystem (from `../../node_modules/@capacitor/filesystem`)"
|
||||
- "CapacitorMlkitBarcodeScanning (from `../../node_modules/@capacitor-mlkit/barcode-scanning`)"
|
||||
@@ -109,8 +98,6 @@ SPEC REPOS:
|
||||
- MLKitVision
|
||||
- nanopb
|
||||
- PromisesObjC
|
||||
- SQLCipher
|
||||
- ZIPFoundation
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
Capacitor:
|
||||
@@ -119,8 +106,6 @@ EXTERNAL SOURCES:
|
||||
:path: "../../node_modules/@capacitor/app"
|
||||
CapacitorCamera:
|
||||
:path: "../../node_modules/@capacitor/camera"
|
||||
CapacitorCommunitySqlite:
|
||||
:path: "../../node_modules/@capacitor-community/sqlite"
|
||||
CapacitorCordova:
|
||||
:path: "../../node_modules/@capacitor/ios"
|
||||
CapacitorFilesystem:
|
||||
@@ -136,7 +121,6 @@ SPEC CHECKSUMS:
|
||||
Capacitor: c95400d761e376be9da6be5a05f226c0e865cebf
|
||||
CapacitorApp: e1e6b7d05e444d593ca16fd6d76f2b7c48b5aea7
|
||||
CapacitorCamera: 9bc7b005d0e6f1d5f525b8137045b60cffffce79
|
||||
CapacitorCommunitySqlite: 0299d20f4b00c2e6aa485a1d8932656753937b9b
|
||||
CapacitorCordova: 8d93e14982f440181be7304aa9559ca631d77fff
|
||||
CapacitorFilesystem: 59270a63c60836248812671aa3b15df673fbaf74
|
||||
CapacitorMlkitBarcodeScanning: 7652be9c7922f39203a361de735d340ae37e134e
|
||||
@@ -154,9 +138,7 @@ SPEC CHECKSUMS:
|
||||
MLKitVision: 90922bca854014a856f8b649d1f1f04f63fd9c79
|
||||
nanopb: 438bc412db1928dac798aa6fd75726007be04262
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
SQLCipher: 31878d8ebd27e5c96db0b7cb695c96e9f8ad77da
|
||||
ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c
|
||||
|
||||
PODFILE CHECKSUM: f987510f7383b04a1b09ea8472bdadcd88b6c924
|
||||
PODFILE CHECKSUM: 7e7e09e6937de7f015393aecf2cf7823645689b3
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
29
main.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const { app, BrowserWindow } = require('electron');
|
||||
const path = require('path');
|
||||
|
||||
function createWindow() {
|
||||
const win = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
contextIsolation: false
|
||||
}
|
||||
});
|
||||
|
||||
win.loadFile(path.join(__dirname, 'dist-electron/www/index.html'));
|
||||
}
|
||||
|
||||
app.whenReady().then(createWindow);
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
5871
package-lock.json
generated
17
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "timesafari",
|
||||
"version": "0.5.1",
|
||||
"version": "0.4.6",
|
||||
"description": "Time Safari Application",
|
||||
"author": {
|
||||
"name": "Time Safari Team"
|
||||
@@ -11,7 +11,7 @@
|
||||
"build": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.mts",
|
||||
"lint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src",
|
||||
"lint-fix": "eslint --ext .js,.ts,.vue --ignore-path .gitignore --fix src",
|
||||
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js && node scripts/copy-wasm.js",
|
||||
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js",
|
||||
"test:all": "npm run test:prerequisites && npm run build && npm run test:web && npm run test:mobile",
|
||||
"test:prerequisites": "node scripts/check-prerequisites.js",
|
||||
"test:web": "npx playwright test -c playwright.config-local.ts --trace on",
|
||||
@@ -46,7 +46,6 @@
|
||||
"electron:build-mac-universal": "npm run build:electron-prod && electron-builder --mac --universal"
|
||||
},
|
||||
"dependencies": {
|
||||
"@capacitor-community/sqlite": "6.0.2",
|
||||
"@capacitor-mlkit/barcode-scanning": "^6.0.0",
|
||||
"@capacitor/android": "^6.2.0",
|
||||
"@capacitor/app": "^6.0.0",
|
||||
@@ -64,7 +63,6 @@
|
||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.6",
|
||||
"@jlongster/sql.js": "^1.6.7",
|
||||
"@peculiar/asn1-ecc": "^2.3.8",
|
||||
"@peculiar/asn1-schema": "^2.3.8",
|
||||
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
|
||||
@@ -83,7 +81,6 @@
|
||||
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
||||
"@vueuse/core": "^12.3.0",
|
||||
"@zxing/text-encoding": "^0.9.0",
|
||||
"absurd-sql": "^0.0.54",
|
||||
"asn1-ber": "^1.2.2",
|
||||
"axios": "^1.6.8",
|
||||
"cbor-x": "^1.5.9",
|
||||
@@ -126,6 +123,7 @@
|
||||
"vue-picture-cropper": "^0.7.0",
|
||||
"vue-qrcode-reader": "^5.5.3",
|
||||
"vue-router": "^4.5.0",
|
||||
"wa-sqlite": "github:rhashimoto/wa-sqlite",
|
||||
"web-did-resolver": "^2.0.27",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
@@ -147,9 +145,7 @@
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vue/eslint-config-typescript": "^11.0.3",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"browserify-fs": "^1.0.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"crypto-browserify": "^3.12.1",
|
||||
"electron": "^33.2.1",
|
||||
"electron-builder": "^25.1.8",
|
||||
"eslint": "^8.57.0",
|
||||
@@ -160,18 +156,17 @@
|
||||
"markdownlint": "^0.37.4",
|
||||
"markdownlint-cli": "^0.44.0",
|
||||
"npm-check-updates": "^17.1.13",
|
||||
"path-browserify": "^1.0.1",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.2.5",
|
||||
"rimraf": "^6.0.1",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "~5.2.2",
|
||||
"vite": "^5.2.0",
|
||||
"vite-plugin-pwa": "^1.0.0"
|
||||
"vite-plugin-pwa": "^0.19.8"
|
||||
},
|
||||
"main": "./dist-electron/main.js",
|
||||
"build": {
|
||||
"appId": "app.timesafari.app",
|
||||
"appId": "app.timesafari",
|
||||
"productName": "TimeSafari",
|
||||
"directories": {
|
||||
"output": "dist-electron-packages"
|
||||
@@ -182,7 +177,7 @@
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "dist-electron/www",
|
||||
"from": "dist",
|
||||
"to": "www"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -2,6 +2,5 @@ dependencies:
|
||||
- gradle
|
||||
- java
|
||||
- pod
|
||||
- rubygems.org
|
||||
|
||||
# other dependencies are discovered via package.json & requirements.txt & Gemfile (I'm guessing).
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
eth_keys
|
||||
pywebview
|
||||
pyinstaller>=6.12.0
|
||||
setuptools>=69.0.0 # Required for distutils for electron-builder on macOS
|
||||
# For development
|
||||
watchdog>=3.0.0 # For file watching support
|
||||
@@ -1,9 +1,10 @@
|
||||
const fs = require('fs');
|
||||
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 wwwPath = path.join(electronDistPath, 'www');
|
||||
|
||||
@@ -12,154 +13,231 @@ if (!fs.existsSync(wwwPath)) {
|
||||
fs.mkdirSync(wwwPath, { recursive: true });
|
||||
}
|
||||
|
||||
// Create a platform-specific index.html for Electron
|
||||
const initialIndexContent = `<!DOCTYPE html>
|
||||
<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>`;
|
||||
// Copy web files to www directory
|
||||
fs.cpSync(webDistPath, wwwPath, { recursive: true });
|
||||
|
||||
// Write the Electron-specific 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
|
||||
// Fix asset paths in index.html
|
||||
const indexPath = path.join(wwwPath, 'index.html');
|
||||
if (fs.existsSync(indexPath)) {
|
||||
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');
|
||||
}
|
||||
let indexContent = fs.readFileSync(indexPath, 'utf8');
|
||||
|
||||
// Fix asset paths
|
||||
console.log('Fixing asset paths in index.html...');
|
||||
let modifiedIndexContent = fs.readFileSync(indexPath, 'utf8');
|
||||
modifiedIndexContent = modifiedIndexContent
|
||||
indexContent = indexContent
|
||||
.replace(/\/assets\//g, './assets/')
|
||||
.replace(/href="\//g, 'href="./')
|
||||
.replace(/src="\//g, 'src="./');
|
||||
|
||||
fs.writeFileSync(indexPath, modifiedIndexContent);
|
||||
|
||||
// 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');
|
||||
}
|
||||
fs.writeFileSync(indexPath, indexContent);
|
||||
|
||||
// Check for remaining /assets/ paths
|
||||
console.log('After path fixing, checking for remaining /assets/ paths:', finalContent.includes('/assets/'));
|
||||
console.log('Sample of fixed content:', finalContent.substring(0, 500));
|
||||
console.log('After path fixing, checking for remaining /assets/ paths:', indexContent.includes('/assets/'));
|
||||
console.log('Sample of fixed content:', indexContent.substring(0, 500));
|
||||
|
||||
console.log('Copied and fixed web files in:', wwwPath);
|
||||
|
||||
// Copy main process files
|
||||
console.log('Copying main process files...');
|
||||
// Copy main process files
|
||||
console.log('Copying main process files...');
|
||||
|
||||
// Copy the main process file instead of creating a template
|
||||
const mainSrcPath = path.join(__dirname, '..', 'dist-electron', 'main.js');
|
||||
const mainDestPath = path.join(electronDistPath, 'main.js');
|
||||
// Create the main process file with inlined logger
|
||||
const mainContent = `const { app, BrowserWindow } = require("electron");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
|
||||
if (fs.existsSync(mainSrcPath)) {
|
||||
fs.copyFileSync(mainSrcPath, mainDestPath);
|
||||
console.log('Copied main process file successfully');
|
||||
} else {
|
||||
console.error('Main process file not found at:', mainSrcPath);
|
||||
process.exit(1);
|
||||
// Inline logger implementation
|
||||
const logger = {
|
||||
log: (...args) => console.log(...args),
|
||||
error: (...args) => console.error(...args),
|
||||
info: (...args) => console.info(...args),
|
||||
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!');
|
||||
@@ -51,7 +51,7 @@ const { existsSync } = require('fs');
|
||||
*/
|
||||
function checkCommand(command, errorMessage) {
|
||||
try {
|
||||
execSync(command, { stdio: 'ignore' });
|
||||
execSync(command + ' --version', { stdio: 'ignore' });
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error(`❌ ${errorMessage}`);
|
||||
@@ -164,10 +164,10 @@ function main() {
|
||||
|
||||
// Check required command line tools
|
||||
// These are essential for building and testing the application
|
||||
success &= checkCommand('node --version', 'Node.js is required');
|
||||
success &= checkCommand('npm --version', 'npm is required');
|
||||
success &= checkCommand('gradle --version', 'Gradle is required for Android builds');
|
||||
success &= checkCommand('xcodebuild --help', 'Xcode is required for iOS builds');
|
||||
success &= checkCommand('node', 'Node.js is required');
|
||||
success &= checkCommand('npm', 'npm is required');
|
||||
success &= checkCommand('gradle', 'Gradle is required for Android builds');
|
||||
success &= checkCommand('xcodebuild', 'Xcode is required for iOS builds');
|
||||
|
||||
// Check platform-specific development environments
|
||||
success &= checkAndroidSetup();
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Create public/wasm directory if it doesn't exist
|
||||
const wasmDir = path.join(__dirname, '../public/wasm');
|
||||
if (!fs.existsSync(wasmDir)) {
|
||||
fs.mkdirSync(wasmDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Copy the WASM file from node_modules to public/wasm
|
||||
const sourceFile = path.join(__dirname, '../node_modules/@jlongster/sql.js/dist/sql-wasm.wasm');
|
||||
const targetFile = path.join(wasmDir, 'sql-wasm.wasm');
|
||||
|
||||
fs.copyFileSync(sourceFile, targetFile);
|
||||
console.log('WASM file copied successfully!');
|
||||
@@ -170,7 +170,7 @@ const executeDeeplink = async (url, description, log) => {
|
||||
|
||||
try {
|
||||
// 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
|
||||
|
||||
execSync(`adb shell am start -W -a android.intent.action.VIEW -d "${url}" -c android.intent.category.BROWSABLE`);
|
||||
|
||||
25
src/App.vue
@@ -4,7 +4,7 @@
|
||||
<!-- Messages in the upper-right - https://github.com/emmanuelsw/notiwind -->
|
||||
<NotificationGroup group="alert">
|
||||
<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
|
||||
v-slot="{ notifications, close }"
|
||||
@@ -330,11 +330,8 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component } from "vue-facing-decorator";
|
||||
|
||||
import { NotificationIface, USE_DEXIE_DB } from "./constants/app";
|
||||
import * as databaseUtil from "./db/databaseUtil";
|
||||
import { retrieveSettingsForActiveAccount } from "./db/index";
|
||||
import { logConsoleAndDb } from "./db/databaseUtil";
|
||||
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from "./db/index";
|
||||
import { NotificationIface } from "./constants/app";
|
||||
import { logger } from "./utils/logger";
|
||||
|
||||
interface Settings {
|
||||
@@ -399,11 +396,7 @@ export default class App extends Vue {
|
||||
|
||||
try {
|
||||
logger.log("Retrieving settings for the active account...");
|
||||
let settings: Settings =
|
||||
await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
if (USE_DEXIE_DB) {
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
}
|
||||
const settings: Settings = await retrieveSettingsForActiveAccount();
|
||||
logger.log("Retrieved settings:", settings);
|
||||
|
||||
const notifyingNewActivity = !!settings?.notifyingNewActivityTime;
|
||||
@@ -548,13 +541,13 @@ export default class App extends Vue {
|
||||
|
||||
<style>
|
||||
#Content {
|
||||
padding-left: max(1.5rem, env(safe-area-inset-left));
|
||||
padding-right: max(1.5rem, env(safe-area-inset-right));
|
||||
padding-top: max(1.5rem, env(safe-area-inset-top));
|
||||
padding-bottom: max(1.5rem, env(safe-area-inset-bottom));
|
||||
padding-left: 1.5rem;
|
||||
padding-right: 1.5rem;
|
||||
padding-top: calc(env(safe-area-inset-top) + 1.5rem);
|
||||
padding-bottom: calc(env(safe-area-inset-bottom) + 1.5rem);
|
||||
}
|
||||
|
||||
#QuickNav ~ #Content {
|
||||
padding-bottom: calc(env(safe-area-inset-bottom) + 6.333rem);
|
||||
padding-bottom: calc(env(safe-area-inset-bottom) + 6rem);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<router-link
|
||||
v-if="record.issuerDid && !isHiddenDid(record.issuerDid)"
|
||||
:to="{
|
||||
path: '/did/' + encodeURIComponent(record.issuerDid),
|
||||
}"
|
||||
title="More details about this person"
|
||||
>
|
||||
<div v-if="record.issuerDid">
|
||||
<EntityIcon
|
||||
:entity-id="record.issuerDid"
|
||||
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
|
||||
/>
|
||||
</router-link>
|
||||
<font-awesome
|
||||
v-else-if="isHiddenDid(record.issuerDid)"
|
||||
icon="eye-slash"
|
||||
class="text-slate-400 !size-[2rem] cursor-pointer"
|
||||
@click="notifyHiddenPerson"
|
||||
/>
|
||||
<font-awesome
|
||||
v-else
|
||||
icon="person-circle-question"
|
||||
class="text-slate-400 !size-[2rem] cursor-pointer"
|
||||
@click="notifyUnknownPerson"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<font-awesome
|
||||
icon="person-circle-question"
|
||||
class="text-slate-300 text-[2rem]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 v-if="record.issuer.known" class="font-semibold leading-tight">
|
||||
{{ record.issuer.displayName }}
|
||||
<h3 class="font-semibold">
|
||||
{{ record.issuer.known ? record.issuer.displayName : "" }}
|
||||
</h3>
|
||||
<p class="ms-auto text-xs text-slate-500 italic">
|
||||
{{ friendlyDate }}
|
||||
@@ -49,11 +37,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
class="cursor-pointer"
|
||||
data-testid="circle-info-link"
|
||||
@click="$emit('loadClaim', record.jwtId)"
|
||||
>
|
||||
<a class="cursor-pointer" @click="$emit('loadClaim', record.jwtId)">
|
||||
<font-awesome icon="circle-info" class="fa-fw text-slate-500" />
|
||||
</a>
|
||||
</div>
|
||||
@@ -62,7 +46,7 @@
|
||||
<!-- Record Image -->
|
||||
<div
|
||||
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});`"
|
||||
>
|
||||
<a
|
||||
@@ -78,59 +62,29 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<p class="font-medium">
|
||||
<a class="cursor-pointer" @click="$emit('loadClaim', record.jwtId)">
|
||||
{{ description }}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<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 -->
|
||||
<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>
|
||||
<!-- Project Icon -->
|
||||
<div v-if="record.providerPlanName">
|
||||
<router-link
|
||||
:to="{
|
||||
path:
|
||||
'/project/' +
|
||||
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>
|
||||
<ProjectIcon
|
||||
:entity-id="record.providerPlanName"
|
||||
:icon-size="48"
|
||||
class="rounded size-[3rem] sm:size-[4rem] *:w-full *:h-full"
|
||||
/>
|
||||
</div>
|
||||
<!-- Identicon for DIDs -->
|
||||
<div v-else-if="record.agentDid">
|
||||
<router-link
|
||||
v-if="!isHiddenDid(record.agentDid)"
|
||||
:to="{
|
||||
path: '/did/' + encodeURIComponent(record.agentDid),
|
||||
}"
|
||||
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"
|
||||
<EntityIcon
|
||||
:entity-id="record.agentDid"
|
||||
:profile-image-url="record.issuer.profileImageUrl"
|
||||
class="rounded-full bg-slate-100 overflow-hidden !size-[3rem] sm:!size-[4rem]"
|
||||
/>
|
||||
</div>
|
||||
<!-- Unknown Person -->
|
||||
@@ -138,7 +92,6 @@
|
||||
<font-awesome
|
||||
icon="person-circle-question"
|
||||
class="text-slate-300 text-[3rem] sm:text-[4rem]"
|
||||
@click="notifyUnknownPerson"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -157,11 +110,9 @@
|
||||
|
||||
<!-- Arrow -->
|
||||
<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
|
||||
class="text-sm text-center leading-none font-semibold pe-2 sm:pe-4"
|
||||
>
|
||||
<div class="text-sm text-center leading-none font-semibold pe-[15px]">
|
||||
{{ fetchAmount }}
|
||||
</div>
|
||||
|
||||
@@ -178,47 +129,24 @@
|
||||
|
||||
<!-- Destination -->
|
||||
<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>
|
||||
<!-- Project Icon -->
|
||||
<div v-if="record.recipientProjectName">
|
||||
<router-link
|
||||
:to="{
|
||||
path:
|
||||
'/project/' +
|
||||
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>
|
||||
<ProjectIcon
|
||||
:entity-id="record.recipientProjectName"
|
||||
:icon-size="48"
|
||||
class="rounded size-[3rem] sm:size-[4rem] *:w-full *:h-full"
|
||||
/>
|
||||
</div>
|
||||
<!-- Identicon for DIDs -->
|
||||
<div v-else-if="record.recipientDid">
|
||||
<router-link
|
||||
v-if="!isHiddenDid(record.recipientDid)"
|
||||
:to="{
|
||||
path: '/did/' + encodeURIComponent(record.recipientDid),
|
||||
}"
|
||||
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"
|
||||
<EntityIcon
|
||||
:entity-id="record.recipientDid"
|
||||
:profile-image-url="record.receiver.profileImageUrl"
|
||||
class="rounded-full bg-slate-100 overflow-hidden !size-[3rem] sm:!size-[4rem]"
|
||||
/>
|
||||
</div>
|
||||
<!-- Unknown Person -->
|
||||
@@ -226,7 +154,6 @@
|
||||
<font-awesome
|
||||
icon="person-circle-question"
|
||||
class="text-slate-300 text-[3rem] sm:text-[4rem]"
|
||||
@click="notifyUnknownPerson"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -243,6 +170,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<p class="font-medium">
|
||||
<a class="cursor-pointer" @click="$emit('loadClaim', record.jwtId)">
|
||||
{{ description }}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
@@ -252,9 +186,8 @@ import { Component, Prop, Vue, Emit } from "vue-facing-decorator";
|
||||
import { GiveRecordWithContactInfo } from "../types";
|
||||
import EntityIcon from "./EntityIcon.vue";
|
||||
import { isGiveClaimType, notifyWhyCannotConfirm } from "../libs/util";
|
||||
import { containsHiddenDid, isHiddenDid } from "../libs/endorserServer";
|
||||
import { containsHiddenDid } from "../libs/endorserServer";
|
||||
import ProjectIcon from "./ProjectIcon.vue";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
@@ -269,33 +202,6 @@ export default class ActivityListItem extends Vue {
|
||||
@Prop() activeDid!: 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()
|
||||
cacheImage(image: string) {
|
||||
return image;
|
||||
@@ -316,7 +222,7 @@ export default class ActivityListItem extends Vue {
|
||||
const claim =
|
||||
(this.record.fullClaim as unknown).claim || this.record.fullClaim;
|
||||
|
||||
return `${claim?.description || ""}`;
|
||||
return `${claim.description}`;
|
||||
}
|
||||
|
||||
private displayAmount(code: string, amt: number) {
|
||||
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
@click="exportDatabase()"
|
||||
>
|
||||
Download Contacts
|
||||
Download Settings & Contacts
|
||||
<br />
|
||||
(excluding Identifier Data)
|
||||
</button>
|
||||
<a
|
||||
ref="downloadLink"
|
||||
@@ -60,18 +62,14 @@ backup and database export, with platform-specific download instructions. * *
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-facing-decorator";
|
||||
import { AppString, NotificationIface } from "../constants/app";
|
||||
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import { db } from "../db/index";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
||||
import {
|
||||
PlatformService,
|
||||
PlatformCapabilities,
|
||||
} from "../services/PlatformService";
|
||||
import { contactsToExportJson } from "../libs/util";
|
||||
|
||||
/**
|
||||
* @vue-component
|
||||
@@ -133,25 +131,21 @@ export default class DataExportSection extends Vue {
|
||||
*/
|
||||
public async exportDatabase() {
|
||||
try {
|
||||
let allContacts: Contact[] = [];
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const result = await platformService.dbQuery(`SELECT * FROM contacts`);
|
||||
if (result) {
|
||||
allContacts = databaseUtil.mapQueryResultToValues(
|
||||
result,
|
||||
) as unknown as Contact[];
|
||||
}
|
||||
// if (USE_DEXIE_DB) {
|
||||
// await db.open();
|
||||
// allContacts = await db.contacts.toArray();
|
||||
// }
|
||||
|
||||
// Convert contacts to export format
|
||||
const exportData = contactsToExportJson(allContacts);
|
||||
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`;
|
||||
const blob = await db.export({
|
||||
prettyJson: true,
|
||||
transform: (table, value, key) => {
|
||||
if (table === "contacts") {
|
||||
// Dexie inserts a number 0 when some are undefined, so we need to totally remove them.
|
||||
Object.keys(value).forEach(prop => {
|
||||
if (value[prop] === undefined) {
|
||||
delete value[prop];
|
||||
}
|
||||
});
|
||||
}
|
||||
return { value, key };
|
||||
},
|
||||
});
|
||||
const fileName = `${db.name}-backup.json`;
|
||||
|
||||
if (this.platformCapabilities.hasFileDownload) {
|
||||
// Web platform: Use download link
|
||||
@@ -163,9 +157,8 @@ export default class DataExportSection extends Vue {
|
||||
setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000);
|
||||
} else if (this.platformCapabilities.hasFileSystem) {
|
||||
// Native platform: Write to app directory
|
||||
await this.platformService.writeAndShareFile(fileName, jsonStr);
|
||||
} else {
|
||||
throw new Error("This platform does not support file downloads.");
|
||||
const content = await blob.text();
|
||||
await this.platformService.writeAndShareFile(fileName, content);
|
||||
}
|
||||
|
||||
this.$notify(
|
||||
@@ -174,10 +167,10 @@ export default class DataExportSection extends Vue {
|
||||
type: "success",
|
||||
title: "Export Successful",
|
||||
text: this.platformCapabilities.hasFileDownload
|
||||
? "See your downloads directory for the backup."
|
||||
: "The backup file has been saved.",
|
||||
? "See your downloads directory for the backup. It is in the Dexie format."
|
||||
: "You should have been prompted to save your backup file.",
|
||||
},
|
||||
3000,
|
||||
-1,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error("Export Error:", error);
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -99,9 +99,6 @@ import {
|
||||
LTileLayer,
|
||||
} from "@vue-leaflet/vue-leaflet";
|
||||
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";
|
||||
|
||||
@@ -125,10 +122,7 @@ export default class FeedFilters extends Vue {
|
||||
async open(onCloseIfChanged: () => void) {
|
||||
this.onCloseIfChanged = onCloseIfChanged;
|
||||
|
||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
if (USE_DEXIE_DB) {
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
}
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.hasVisibleDid = !!settings.filterFeedByVisible;
|
||||
this.isNearby = !!settings.filterFeedByNearby;
|
||||
if (settings.searchBoxes && settings.searchBoxes.length > 0) {
|
||||
@@ -142,29 +136,17 @@ export default class FeedFilters extends Vue {
|
||||
async toggleHasVisibleDid() {
|
||||
this.settingChanged = true;
|
||||
this.hasVisibleDid = !this.hasVisibleDid;
|
||||
await databaseUtil.updateDefaultSettings({
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
filterFeedByVisible: this.hasVisibleDid,
|
||||
});
|
||||
|
||||
if (USE_DEXIE_DB) {
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
filterFeedByVisible: this.hasVisibleDid,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async toggleNearby() {
|
||||
this.settingChanged = true;
|
||||
this.isNearby = !this.isNearby;
|
||||
await databaseUtil.updateDefaultSettings({
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
filterFeedByNearby: this.isNearby,
|
||||
});
|
||||
|
||||
if (USE_DEXIE_DB) {
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
filterFeedByNearby: this.isNearby,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async clearAll() {
|
||||
@@ -172,18 +154,11 @@ export default class FeedFilters extends Vue {
|
||||
this.settingChanged = true;
|
||||
}
|
||||
|
||||
await databaseUtil.updateDefaultSettings({
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
filterFeedByNearby: false,
|
||||
filterFeedByVisible: false,
|
||||
});
|
||||
|
||||
if (USE_DEXIE_DB) {
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
filterFeedByNearby: false,
|
||||
filterFeedByVisible: false,
|
||||
});
|
||||
}
|
||||
|
||||
this.hasVisibleDid = false;
|
||||
this.isNearby = false;
|
||||
}
|
||||
@@ -193,18 +168,11 @@ export default class FeedFilters extends Vue {
|
||||
this.settingChanged = true;
|
||||
}
|
||||
|
||||
await databaseUtil.updateDefaultSettings({
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
filterFeedByNearby: true,
|
||||
filterFeedByVisible: true,
|
||||
});
|
||||
|
||||
if (USE_DEXIE_DB) {
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
filterFeedByNearby: true,
|
||||
filterFeedByVisible: true,
|
||||
});
|
||||
}
|
||||
|
||||
this.hasVisibleDid = true;
|
||||
this.isNearby = true;
|
||||
}
|
||||
|
||||
@@ -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 & more options…
|
||||
</router-link>
|
||||
|
||||
<!-- Sign & Send Info -->
|
||||
<p class="text-center text-sm mb-4">
|
||||
<b class="font-medium">Sign & 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 & 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>
|
||||
@@ -1,108 +1,112 @@
|
||||
<template>
|
||||
<div v-if="visible" class="dialog-overlay">
|
||||
<div class="dialog">
|
||||
<!-- Step 1: Entity Selection -->
|
||||
<EntitySelectionStep
|
||||
v-show="currentStep === 1"
|
||||
: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"
|
||||
/>
|
||||
|
||||
<!-- 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"
|
||||
<h1 class="text-xl font-bold text-center mb-4">
|
||||
{{ customTitle }}
|
||||
</h1>
|
||||
<input
|
||||
v-model="description"
|
||||
type="text"
|
||||
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
|
||||
:placeholder="prompt || 'What was given?'"
|
||||
/>
|
||||
<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 & 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>
|
||||
</template>
|
||||
|
||||
<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 {
|
||||
createAndSubmitGive,
|
||||
didInfo,
|
||||
serverMessageForUser,
|
||||
getHeaders,
|
||||
} from "../libs/endorserServer";
|
||||
import * as libsUtil from "../libs/util";
|
||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
import { retrieveAccountDids } from "../libs/util";
|
||||
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({
|
||||
components: {
|
||||
EntityIcon,
|
||||
ProjectIcon,
|
||||
EntitySelectionStep,
|
||||
GiftDetailsStep,
|
||||
},
|
||||
})
|
||||
@Component
|
||||
export default class GiftedDialog extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
|
||||
@Prop() fromProjectId = "";
|
||||
@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 = "";
|
||||
allContacts: Array<Contact> = [];
|
||||
@@ -119,94 +123,9 @@ export default class GiftedDialog extends Vue {
|
||||
receiver?: libsUtil.GiverReceiverInputInfo;
|
||||
unitCode = "HUR";
|
||||
visible = false;
|
||||
currentStep = 1;
|
||||
|
||||
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(
|
||||
giver?: libsUtil.GiverReceiverInputInfo,
|
||||
receiver?: libsUtil.GiverReceiverInputInfo,
|
||||
@@ -219,33 +138,17 @@ export default class GiftedDialog extends Vue {
|
||||
this.giver = giver;
|
||||
this.prompt = prompt || "";
|
||||
this.receiver = receiver;
|
||||
// if we show "given to user" selection, default checkbox to true
|
||||
this.amountInput = "0";
|
||||
this.callbackOnSuccess = callbackOnSuccess;
|
||||
this.offerId = offerId || "";
|
||||
this.currentStep = giver ? 2 : 1;
|
||||
this.stepType = "giver";
|
||||
|
||||
// Update entity types based on current props
|
||||
this.updateEntityTypes();
|
||||
|
||||
try {
|
||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
if (USE_DEXIE_DB) {
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
}
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.activeDid = settings.activeDid || "";
|
||||
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
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.allContacts = await db.contacts.toArray();
|
||||
|
||||
this.allMyDids = await retrieveAccountDids();
|
||||
|
||||
@@ -257,16 +160,7 @@ export default class GiftedDialog extends Vue {
|
||||
this.allContacts,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
this.giverEntityType === "project" ||
|
||||
this.recipientEntityType === "project"
|
||||
) {
|
||||
await this.loadProjects();
|
||||
} else {
|
||||
// Clear projects array when not needed
|
||||
this.projects = [];
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (err: any) {
|
||||
logger.error("Error retrieving settings from database:", err);
|
||||
this.$notify(
|
||||
@@ -316,7 +210,6 @@ export default class GiftedDialog extends Vue {
|
||||
this.amountInput = "0";
|
||||
this.prompt = "";
|
||||
this.unitCode = "HUR";
|
||||
this.currentStep = 1;
|
||||
}
|
||||
|
||||
async confirm() {
|
||||
@@ -359,20 +252,6 @@ export default class GiftedDialog extends Vue {
|
||||
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.$notify(
|
||||
{
|
||||
@@ -411,55 +290,26 @@ export default class GiftedDialog extends Vue {
|
||||
unitCode: string = "HUR",
|
||||
) {
|
||||
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(
|
||||
this.axios,
|
||||
this.apiServer,
|
||||
this.activeDid,
|
||||
fromDid,
|
||||
toDid,
|
||||
giverDid as string,
|
||||
recipientDid as string,
|
||||
description,
|
||||
amount,
|
||||
unitCode,
|
||||
fulfillsProjectHandleId,
|
||||
this.toProjectId,
|
||||
this.offerId,
|
||||
false,
|
||||
undefined,
|
||||
providerPlanHandleId,
|
||||
this.fromProjectId,
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
if (
|
||||
result.type === "error" ||
|
||||
this.isGiveCreationError(result.response)
|
||||
) {
|
||||
const errorMessage = this.getGiveCreationErrorMessage(result);
|
||||
logger.error("Error with give creation result:", result);
|
||||
this.$notify(
|
||||
@@ -506,6 +356,15 @@ export default class GiftedDialog extends Vue {
|
||||
|
||||
// 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")
|
||||
* @returns best guess at an error message
|
||||
@@ -530,173 +389,6 @@ export default class GiftedDialog extends Vue {
|
||||
-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>
|
||||
|
||||
|
||||
@@ -74,12 +74,10 @@
|
||||
import { Vue, Component } from "vue-facing-decorator";
|
||||
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 { Contact } from "../db/tables/contacts";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
import { GiverReceiverInputInfo } from "../libs/util";
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
|
||||
@Component
|
||||
export default class GivenPrompts extends Vue {
|
||||
@@ -129,16 +127,8 @@ export default class GivenPrompts extends Vue {
|
||||
this.visible = true;
|
||||
this.callbackOnFullGiftInfo = callbackOnFullGiftInfo;
|
||||
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const result = await platformService.dbQuery(
|
||||
"SELECT COUNT(*) FROM contacts",
|
||||
);
|
||||
if (result) {
|
||||
this.numContacts = result.values[0][0] as number;
|
||||
}
|
||||
if (USE_DEXIE_DB) {
|
||||
this.numContacts = await db.contacts.count();
|
||||
}
|
||||
await db.open();
|
||||
this.numContacts = await db.contacts.count();
|
||||
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 count = 0;
|
||||
|
||||
// as long as the index has an entry, loop
|
||||
while (
|
||||
this.shownContactDbIndices[someContactDbIndex] != null &&
|
||||
@@ -240,21 +229,10 @@ export default class GivenPrompts extends Vue {
|
||||
this.nextIdeaPastContacts();
|
||||
} else {
|
||||
// get the contact at that offset
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const result = await platformService.dbQuery(
|
||||
"SELECT * FROM contacts LIMIT 1 OFFSET ?",
|
||||
[someContactDbIndex],
|
||||
);
|
||||
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();
|
||||
}
|
||||
await db.open();
|
||||
this.currentContact = await db.contacts
|
||||
.offset(someContactDbIndex)
|
||||
.first();
|
||||
this.shownContactDbIndices[someContactDbIndex] = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,11 @@
|
||||
<span>
|
||||
{{ didInfo(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
|
||||
icon="arrow-up-right-from-square"
|
||||
class="fa-fw"
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
<div class="text-lg text-center font-bold relative">
|
||||
<h1 id="ViewHeading" class="text-center font-bold">
|
||||
<span v-if="uploading">Uploading Image…</span>
|
||||
<span v-else-if="blob">{{
|
||||
crop ? "Crop Image" : "Preview Image"
|
||||
}}</span>
|
||||
<span v-else-if="blob">Crop Image</span>
|
||||
<span v-else-if="showCameraPreview">Upload Image</span>
|
||||
<span v-else>Add Photo</span>
|
||||
</h1>
|
||||
@@ -121,23 +119,12 @@
|
||||
playsinline
|
||||
muted
|
||||
></video>
|
||||
<div
|
||||
class="absolute bottom-4 inset-x-0 flex items-center justify-center gap-4"
|
||||
<button
|
||||
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
|
||||
class="bg-white text-slate-800 p-3 rounded-full text-2xl leading-none"
|
||||
@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>
|
||||
<font-awesome icon="camera" class="w-[1em]" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -242,12 +229,12 @@
|
||||
<p class="mb-2">
|
||||
Before you can upload a photo, a friend needs to register you.
|
||||
</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"
|
||||
@click="handleQRCodeClick"
|
||||
>
|
||||
Share Your Info
|
||||
</button>
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -260,17 +247,11 @@ import axios from "axios";
|
||||
import { ref } from "vue";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import VuePictureCropper, { cropper } from "vue-picture-cropper";
|
||||
import { Capacitor } from "@capacitor/core";
|
||||
import {
|
||||
DEFAULT_IMAGE_API_SERVER,
|
||||
NotificationIface,
|
||||
USE_DEXIE_DB,
|
||||
} from "../constants/app";
|
||||
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
|
||||
import { retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { accessToken } from "../libs/crypto";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
|
||||
const inputImageFileNameRef = ref<Blob>();
|
||||
|
||||
@@ -281,11 +262,6 @@ const inputImageFileNameRef = ref<Blob>();
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
defaultCameraMode: {
|
||||
type: String,
|
||||
default: "environment",
|
||||
validator: (value: string) => ["environment", "user"].includes(value),
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class ImageMethodDialog extends Vue {
|
||||
@@ -327,9 +303,6 @@ export default class ImageMethodDialog extends Vue {
|
||||
/** Camera stream reference */
|
||||
private cameraStream: MediaStream | null = null;
|
||||
|
||||
/** Current camera facing mode */
|
||||
private currentFacingMode: "environment" | "user" = "environment";
|
||||
|
||||
private platformService = PlatformServiceFactory.getInstance();
|
||||
URL = window.URL || window.webkitURL;
|
||||
|
||||
@@ -360,11 +333,9 @@ export default class ImageMethodDialog extends Vue {
|
||||
* @throws {Error} When settings retrieval fails
|
||||
*/
|
||||
async mounted() {
|
||||
logger.log("ImageMethodDialog mounted");
|
||||
try {
|
||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
if (USE_DEXIE_DB) {
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
}
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
} catch (error: unknown) {
|
||||
logger.error("Error retrieving settings from database:", error);
|
||||
@@ -391,16 +362,15 @@ export default class ImageMethodDialog extends Vue {
|
||||
}
|
||||
|
||||
open(setImageFn: (arg: string) => void, claimType: string, crop?: boolean) {
|
||||
logger.debug("ImageMethodDialog.open called");
|
||||
this.claimType = claimType;
|
||||
this.crop = !!crop;
|
||||
this.imageCallback = setImageFn;
|
||||
this.visible = true;
|
||||
this.currentFacingMode = this.defaultCameraMode as "environment" | "user";
|
||||
|
||||
// Start camera preview immediately
|
||||
logger.debug("Starting camera preview from open()");
|
||||
this.startCameraPreview();
|
||||
// Start camera preview immediately if not on mobile
|
||||
if (!this.platformCapabilities.isNativeApp) {
|
||||
this.startCameraPreview();
|
||||
}
|
||||
}
|
||||
|
||||
async uploadImageFile(event: Event) {
|
||||
@@ -469,24 +439,46 @@ export default class ImageMethodDialog extends Vue {
|
||||
logger.debug("startCameraPreview called");
|
||||
logger.debug("Current showCameraPreview state:", this.showCameraPreview);
|
||||
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 {
|
||||
this.cameraState = "initializing";
|
||||
this.cameraStateMessage = "Requesting camera access...";
|
||||
this.showCameraPreview = true;
|
||||
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({
|
||||
video: { facingMode: this.currentFacingMode },
|
||||
video: { facingMode: "environment" },
|
||||
});
|
||||
logger.debug("Camera access granted");
|
||||
this.cameraStream = stream;
|
||||
@@ -500,36 +492,25 @@ export default class ImageMethodDialog extends Vue {
|
||||
videoElement.srcObject = stream;
|
||||
await new Promise((resolve) => {
|
||||
videoElement.onloadedmetadata = () => {
|
||||
videoElement
|
||||
.play()
|
||||
.then(() => {
|
||||
logger.debug("Video element started playing");
|
||||
resolve(true);
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error("Error playing video:", error);
|
||||
throw error;
|
||||
});
|
||||
videoElement.play().then(() => {
|
||||
resolve(true);
|
||||
});
|
||||
};
|
||||
});
|
||||
} else {
|
||||
logger.error("Video element not found");
|
||||
throw new Error("Video element not found");
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error starting camera preview:", error);
|
||||
let errorMessage =
|
||||
error instanceof Error ? error.message : "Failed to access camera";
|
||||
if (
|
||||
error instanceof Error &&
|
||||
(error.name === "NotReadableError" || error.name === "TrackStartError")
|
||||
error.name === "NotReadableError" ||
|
||||
error.name === "TrackStartError"
|
||||
) {
|
||||
errorMessage =
|
||||
"Camera is in use by another application. Please close any other apps or browser tabs using the camera and try again.";
|
||||
} else if (
|
||||
error instanceof Error &&
|
||||
(error.name === "NotAllowedError" ||
|
||||
error.name === "PermissionDeniedError")
|
||||
error.name === "NotAllowedError" ||
|
||||
error.name === "PermissionDeniedError"
|
||||
) {
|
||||
errorMessage =
|
||||
"Camera access was denied. Please allow camera access in your browser settings.";
|
||||
@@ -537,7 +518,6 @@ export default class ImageMethodDialog extends Vue {
|
||||
this.cameraState = "error";
|
||||
this.cameraStateMessage = errorMessage;
|
||||
this.error = errorMessage;
|
||||
this.showCameraPreview = false;
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -547,6 +527,7 @@ export default class ImageMethodDialog extends Vue {
|
||||
},
|
||||
5000,
|
||||
);
|
||||
this.showCameraPreview = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -598,21 +579,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 {
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
@@ -647,7 +613,6 @@ export default class ImageMethodDialog extends Vue {
|
||||
5000,
|
||||
);
|
||||
this.uploading = false;
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
formData.append("image", this.blob, this.fileName || "photo.jpg");
|
||||
@@ -702,7 +667,6 @@ export default class ImageMethodDialog extends Vue {
|
||||
);
|
||||
this.uploading = false;
|
||||
this.blob = undefined;
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -710,14 +674,6 @@ export default class ImageMethodDialog extends Vue {
|
||||
toggleDiagnostics() {
|
||||
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>
|
||||
|
||||
|
||||
@@ -172,10 +172,8 @@ import {
|
||||
} from "../libs/endorserServer";
|
||||
import { decryptMessage } from "../libs/crypto";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
import * as libsUtil from "../libs/util";
|
||||
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
|
||||
interface Member {
|
||||
admitted: boolean;
|
||||
@@ -211,10 +209,7 @@ export default class MembersList extends Vue {
|
||||
contacts: Array<Contact> = [];
|
||||
|
||||
async created() {
|
||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
if (USE_DEXIE_DB) {
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
}
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.firstName = settings.firstName || "";
|
||||
@@ -301,7 +296,7 @@ export default class MembersList extends Vue {
|
||||
this.decryptedMembers.length === 0 ||
|
||||
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 {
|
||||
// the first (organizer) member was decrypted OK
|
||||
return "";
|
||||
@@ -342,7 +337,7 @@ export default class MembersList extends Vue {
|
||||
group: "alert",
|
||||
type: "info",
|
||||
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,
|
||||
);
|
||||
@@ -352,7 +347,7 @@ export default class MembersList extends Vue {
|
||||
group: "alert",
|
||||
type: "info",
|
||||
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,
|
||||
);
|
||||
@@ -360,16 +355,7 @@ export default class MembersList extends Vue {
|
||||
}
|
||||
|
||||
async loadContacts() {
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
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();
|
||||
}
|
||||
this.contacts = await db.contacts.toArray();
|
||||
}
|
||||
|
||||
getContactFor(did: string): Contact | undefined {
|
||||
@@ -453,14 +439,7 @@ export default class MembersList extends Vue {
|
||||
if (result.success) {
|
||||
decrMember.isRegistered = true;
|
||||
if (oldContact) {
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
await platformService.dbExec(
|
||||
"UPDATE contacts SET registered = ? WHERE did = ?",
|
||||
[true, decrMember.did],
|
||||
);
|
||||
if (USE_DEXIE_DB) {
|
||||
await db.contacts.update(decrMember.did, { registered: true });
|
||||
}
|
||||
await db.contacts.update(decrMember.did, { registered: true });
|
||||
oldContact.registered = true;
|
||||
}
|
||||
this.$notify(
|
||||
@@ -513,14 +492,7 @@ export default class MembersList extends Vue {
|
||||
name: member.name,
|
||||
};
|
||||
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
await platformService.dbExec(
|
||||
"INSERT INTO contacts (did, name) VALUES (?, ?)",
|
||||
[member.did, member.name],
|
||||
);
|
||||
if (USE_DEXIE_DB) {
|
||||
await db.contacts.add(newContact);
|
||||
}
|
||||
await db.contacts.add(newContact);
|
||||
this.contacts.push(newContact);
|
||||
|
||||
this.$notify(
|
||||
|
||||
@@ -82,13 +82,12 @@
|
||||
<script lang="ts">
|
||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||
|
||||
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import {
|
||||
createAndSubmitOffer,
|
||||
serverMessageForUser,
|
||||
} from "../libs/endorserServer";
|
||||
import * as libsUtil from "../libs/util";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
import { retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
@@ -117,10 +116,7 @@ export default class OfferDialog extends Vue {
|
||||
this.recipientDid = recipientDid;
|
||||
this.recipientName = recipientName;
|
||||
|
||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
if (USE_DEXIE_DB) {
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
}
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.activeDid = settings.activeDid || "";
|
||||
|
||||
@@ -249,7 +245,10 @@ export default class OfferDialog extends Vue {
|
||||
this.projectId,
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
if (
|
||||
result.type === "error" ||
|
||||
this.isOfferCreationError(result.response)
|
||||
) {
|
||||
const errorMessage = this.getOfferCreationErrorMessage(result);
|
||||
logger.error("Error with offer creation result:", result);
|
||||
this.$notify(
|
||||
@@ -293,6 +292,15 @@ export default class OfferDialog extends Vue {
|
||||
|
||||
// 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")
|
||||
* @returns best guess at an error message
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<h1 class="text-xl font-bold text-center mb-4 relative">
|
||||
Welcome to Time Safari
|
||||
<br />
|
||||
- Showcase Impact & Magnify Time
|
||||
- Showcasing Gratitude & Magnifying Time
|
||||
<div
|
||||
class="text-lg text-center leading-none absolute right-0 -top-1"
|
||||
@click="onClickClose(true)"
|
||||
@@ -14,9 +14,6 @@
|
||||
</div>
|
||||
</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">
|
||||
You can now log things that you've seen:
|
||||
<span v-if="numContacts > 0">
|
||||
@@ -26,10 +23,14 @@
|
||||
<span class="bg-green-600 text-white rounded-full">
|
||||
<font-awesome icon="plus" class="fa-fw" />
|
||||
</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 class="mt-4">
|
||||
Once someone registers you, you can log your appreciation, too.
|
||||
<p v-else class="mt-4">
|
||||
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 class="mt-4">
|
||||
@@ -200,16 +201,13 @@
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { Router } from "vue-router";
|
||||
|
||||
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import {
|
||||
db,
|
||||
retrieveSettingsForActiveAccount,
|
||||
updateAccountSettings,
|
||||
} from "../db/index";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
import { OnboardPage } from "../libs/util";
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
|
||||
@Component({
|
||||
computed: {
|
||||
@@ -224,7 +222,7 @@ export default class OnboardingDialog extends Vue {
|
||||
$router!: Router;
|
||||
|
||||
activeDid = "";
|
||||
firstContactName = "";
|
||||
firstContactName = null;
|
||||
givenName = "";
|
||||
isRegistered = false;
|
||||
numContacts = 0;
|
||||
@@ -233,54 +231,29 @@ export default class OnboardingDialog extends Vue {
|
||||
|
||||
async open(page: OnboardPage) {
|
||||
this.page = page;
|
||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
if (USE_DEXIE_DB) {
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
}
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
this.activeDid = settings.activeDid || "";
|
||||
this.isRegistered = !!settings.isRegistered;
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const dbContacts = await platformService.dbQuery("SELECT * FROM contacts");
|
||||
if (dbContacts) {
|
||||
this.numContacts = dbContacts.values.length;
|
||||
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 || "";
|
||||
}
|
||||
const contacts = await db.contacts.toArray();
|
||||
this.numContacts = contacts.length;
|
||||
if (this.numContacts > 0) {
|
||||
this.firstContactName = contacts[0].name;
|
||||
}
|
||||
this.visible = true;
|
||||
if (this.page === OnboardPage.Create) {
|
||||
// we'll assume that they've been through all the other pages
|
||||
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
|
||||
await updateAccountSettings(this.activeDid, {
|
||||
finishedOnboarding: true,
|
||||
});
|
||||
if (USE_DEXIE_DB) {
|
||||
await updateAccountSettings(this.activeDid, {
|
||||
finishedOnboarding: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async onClickClose(done?: boolean, goHome?: boolean) {
|
||||
this.visible = false;
|
||||
if (done) {
|
||||
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
|
||||
await updateAccountSettings(this.activeDid, {
|
||||
finishedOnboarding: true,
|
||||
});
|
||||
if (USE_DEXIE_DB) {
|
||||
await updateAccountSettings(this.activeDid, {
|
||||
finishedOnboarding: true,
|
||||
});
|
||||
}
|
||||
if (goHome) {
|
||||
this.$router.push({ name: "home" });
|
||||
}
|
||||
|
||||
@@ -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>
|
||||