Compare commits
66 Commits
electron-c
...
sql-absurd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
981920dd7a | ||
|
|
d189c39062 | ||
|
|
8edddb1a57 | ||
|
|
9eb07b3258 | ||
|
|
e5dffc30ff | ||
|
|
0b4e885edd | ||
|
|
b6d9b29720 | ||
|
|
b5348e42a7 | ||
|
|
a4fb3eea2d | ||
|
|
5d12c76693 | ||
|
|
d426f9c4ac | ||
| 340a574325 | |||
|
|
98b3a35e3c | ||
|
|
409de21fc4 | ||
|
|
17c9d32f49 | ||
|
|
25e4db395a | ||
|
|
b6ee30892f | ||
|
|
b01a450733 | ||
|
|
596f3355bf | ||
|
|
e1f9a6fa08 | ||
|
|
340e718199 | ||
|
|
5d97c98ae8 | ||
|
|
ec74fff892 | ||
|
|
1e88c0e26f | ||
|
|
3ec2364394 | ||
|
|
8b215c909d | ||
|
|
91a1c05473 | ||
|
|
66929d9b14 | ||
|
|
1e63ddcb6e | ||
|
|
51f5755f5c | ||
|
|
e5a3d622b6 | ||
|
|
a6edcd6269 | ||
|
|
b7b6be5831 | ||
|
|
cbaca0304d | ||
| 59d711bd90 | |||
|
|
c355de6e33 | ||
|
|
28c114a2c7 | ||
| dabfe33fbe | |||
| d8f2587d1c | |||
|
|
3946a8a27a | ||
| 4c40b80718 | |||
| 74989c2b64 | |||
| 7e17b41444 | |||
| 83acb028c7 | |||
|
|
786f07e067 | ||
|
|
710cc1683c | ||
|
|
ebef5d6c8d | ||
|
|
43ea7ee610 | ||
|
|
57191df416 | ||
| 644593a5f4 | |||
|
|
900c2521c7 | ||
|
|
182cff2b16 | ||
|
|
3b4ef908f3 | ||
|
|
a5a9e15ece | ||
|
|
a6d8f0eb8a | ||
|
|
3997a88b44 | ||
| 5eeeae32c6 | |||
|
|
d9895086e6 | ||
|
|
fb8d1cb8b2 | ||
|
|
70c0edbed0 | ||
|
|
55cc08d675 | ||
|
|
688a5be76e | ||
|
|
014341f320 | ||
|
|
1d5e062c76 | ||
|
|
2c5c15108a | ||
|
|
26df0fb671 |
@@ -1,101 +0,0 @@
|
|||||||
VM5:29 [Preload] Preload script starting...
|
|
||||||
VM5:29 [Preload] Preload script completed successfully
|
|
||||||
main.common-DiOUyXe7.js:27 Platform Object
|
|
||||||
error @ main.common-DiOUyXe7.js:27
|
|
||||||
main.common-DiOUyXe7.js:27 PWA enabled Object
|
|
||||||
error @ main.common-DiOUyXe7.js:27
|
|
||||||
main.common-DiOUyXe7.js:27 [Web] PWA enabled Object
|
|
||||||
error @ main.common-DiOUyXe7.js:27
|
|
||||||
main.common-DiOUyXe7.js:27 [Web] Platform Object
|
|
||||||
error @ main.common-DiOUyXe7.js:27
|
|
||||||
main.common-DiOUyXe7.js:29 Opened!
|
|
||||||
main.common-DiOUyXe7.js:2552 Failed to log to database: Error: no such column: value
|
|
||||||
at E.handleError (main.common-DiOUyXe7.js:27:21133)
|
|
||||||
at E.exec (main.common-DiOUyXe7.js:27:19785)
|
|
||||||
at Rc.processQueue (main.common-DiOUyXe7.js:2379:2368)
|
|
||||||
F7 @ main.common-DiOUyXe7.js:2552
|
|
||||||
main.common-DiOUyXe7.js:2552 Original message: PWA enabled - [{"pwa_enabled":false}]
|
|
||||||
F7 @ main.common-DiOUyXe7.js:2552
|
|
||||||
main.common-DiOUyXe7.js:2552 Failed to log to database: Error: no such column: value
|
|
||||||
at E.handleError (main.common-DiOUyXe7.js:27:21133)
|
|
||||||
at E.exec (main.common-DiOUyXe7.js:27:19785)
|
|
||||||
at Rc.processQueue (main.common-DiOUyXe7.js:2379:2368)
|
|
||||||
at main.common-DiOUyXe7.js:2379:2816
|
|
||||||
at new Promise (<anonymous>)
|
|
||||||
at Rc.queueOperation (main.common-DiOUyXe7.js:2379:2685)
|
|
||||||
at Rc.query (main.common-DiOUyXe7.js:2379:3378)
|
|
||||||
at async F7 (main.common-DiOUyXe7.js:2552:117)
|
|
||||||
F7 @ main.common-DiOUyXe7.js:2552
|
|
||||||
main.common-DiOUyXe7.js:2552 Original message: [Web] PWA enabled - [{"pwa_enabled":false}]
|
|
||||||
F7 @ main.common-DiOUyXe7.js:2552
|
|
||||||
main.common-DiOUyXe7.js:2552 Failed to log to database: Error: no such column: value
|
|
||||||
at E.handleError (main.common-DiOUyXe7.js:27:21133)
|
|
||||||
at E.exec (main.common-DiOUyXe7.js:27:19785)
|
|
||||||
at Rc.processQueue (main.common-DiOUyXe7.js:2379:2368)
|
|
||||||
at main.common-DiOUyXe7.js:2379:2816
|
|
||||||
at new Promise (<anonymous>)
|
|
||||||
at Rc.queueOperation (main.common-DiOUyXe7.js:2379:2685)
|
|
||||||
at Rc.query (main.common-DiOUyXe7.js:2379:3378)
|
|
||||||
at async F7 (main.common-DiOUyXe7.js:2552:117)
|
|
||||||
F7 @ main.common-DiOUyXe7.js:2552
|
|
||||||
main.common-DiOUyXe7.js:2552 Original message: [Web] Platform - [{"platform":"web"}]
|
|
||||||
F7 @ main.common-DiOUyXe7.js:2552
|
|
||||||
main.common-DiOUyXe7.js:2552 Failed to log to database: Error: no such column: value
|
|
||||||
at E.handleError (main.common-DiOUyXe7.js:27:21133)
|
|
||||||
at E.exec (main.common-DiOUyXe7.js:27:19785)
|
|
||||||
at Rc.processQueue (main.common-DiOUyXe7.js:2379:2368)
|
|
||||||
at main.common-DiOUyXe7.js:2379:2816
|
|
||||||
at new Promise (<anonymous>)
|
|
||||||
at Rc.queueOperation (main.common-DiOUyXe7.js:2379:2685)
|
|
||||||
at Rc.query (main.common-DiOUyXe7.js:2379:3378)
|
|
||||||
at async F7 (main.common-DiOUyXe7.js:2552:117)
|
|
||||||
F7 @ main.common-DiOUyXe7.js:2552
|
|
||||||
main.common-DiOUyXe7.js:2552 Original message: Platform - [{"platform":"web"}]
|
|
||||||
F7 @ main.common-DiOUyXe7.js:2552
|
|
||||||
main.common-DiOUyXe7.js:2100
|
|
||||||
|
|
||||||
|
|
||||||
GET https://api.endorser.ch/api/report/rateLimits 400 (Bad Request)
|
|
||||||
(anonymous) @ main.common-DiOUyXe7.js:2100
|
|
||||||
xhr @ main.common-DiOUyXe7.js:2100
|
|
||||||
p6 @ main.common-DiOUyXe7.js:2102
|
|
||||||
_request @ main.common-DiOUyXe7.js:2103
|
|
||||||
request @ main.common-DiOUyXe7.js:2102
|
|
||||||
Yc.<computed> @ main.common-DiOUyXe7.js:2103
|
|
||||||
(anonymous) @ main.common-DiOUyXe7.js:2098
|
|
||||||
dJ @ main.common-DiOUyXe7.js:2295
|
|
||||||
main.common-DiOUyXe7.js:2100
|
|
||||||
|
|
||||||
|
|
||||||
GET https://api.endorser.ch/api/report/rateLimits 400 (Bad Request)
|
|
||||||
(anonymous) @ main.common-DiOUyXe7.js:2100
|
|
||||||
xhr @ main.common-DiOUyXe7.js:2100
|
|
||||||
p6 @ main.common-DiOUyXe7.js:2102
|
|
||||||
_request @ main.common-DiOUyXe7.js:2103
|
|
||||||
request @ main.common-DiOUyXe7.js:2102
|
|
||||||
Yc.<computed> @ main.common-DiOUyXe7.js:2103
|
|
||||||
(anonymous) @ main.common-DiOUyXe7.js:2098
|
|
||||||
dJ @ main.common-DiOUyXe7.js:2295
|
|
||||||
await in dJ
|
|
||||||
checkRegistrationStatus @ HomeView-DJMSCuMg.js:1
|
|
||||||
mounted @ HomeView-DJMSCuMg.js:1
|
|
||||||
XMLHttpRequest.send
|
|
||||||
(anonymous) @ main.common-DiOUyXe7.js:2100
|
|
||||||
xhr @ main.common-DiOUyXe7.js:2100
|
|
||||||
p6 @ main.common-DiOUyXe7.js:2102
|
|
||||||
_request @ main.common-DiOUyXe7.js:2103
|
|
||||||
request @ main.common-DiOUyXe7.js:2102
|
|
||||||
Yc.<computed> @ main.common-DiOUyXe7.js:2103
|
|
||||||
(anonymous) @ main.common-DiOUyXe7.js:2098
|
|
||||||
ZG @ main.common-DiOUyXe7.js:2295
|
|
||||||
await in ZG
|
|
||||||
initializeIdentity @ HomeView-DJMSCuMg.js:1
|
|
||||||
XMLHttpRequest.send
|
|
||||||
(anonymous) @ main.common-DiOUyXe7.js:2100
|
|
||||||
xhr @ main.common-DiOUyXe7.js:2100
|
|
||||||
p6 @ main.common-DiOUyXe7.js:2102
|
|
||||||
_request @ main.common-DiOUyXe7.js:2103
|
|
||||||
request @ main.common-DiOUyXe7.js:2102
|
|
||||||
Yc.<computed> @ main.common-DiOUyXe7.js:2103
|
|
||||||
(anonymous) @ main.common-DiOUyXe7.js:2098
|
|
||||||
dJ @ main.common-DiOUyXe7.js:2295
|
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
"appId": "app.timesafari",
|
"appId": "com.timesafari.app",
|
||||||
"appName": "TimeSafari",
|
"appName": "TimeSafari",
|
||||||
"webDir": "dist",
|
"webDir": "dist",
|
||||||
"bundledWebRuntime": false,
|
"bundledWebRuntime": false,
|
||||||
"server": {
|
"server": {
|
||||||
"cleartext": true
|
"cleartext": true,
|
||||||
|
"androidScheme": "https"
|
||||||
},
|
},
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"App": {
|
"App": {
|
||||||
@@ -29,6 +30,12 @@
|
|||||||
"biometricAuth": true,
|
"biometricAuth": true,
|
||||||
"biometricTitle": "Biometric login for TimeSafari"
|
"biometricTitle": "Biometric login for TimeSafari"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"CapacitorSQLite": {
|
||||||
|
"electronIsEncryption": false,
|
||||||
|
"electronMacLocation": "~/Library/Application Support/TimeSafari",
|
||||||
|
"electronWindowsLocation": "C:\\ProgramData\\TimeSafari",
|
||||||
|
"electronLinuxLocation": "~/.local/share/TimeSafari"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ios": {
|
"ios": {
|
||||||
|
|||||||
270
doc/electron-migration.md
Normal file
270
doc/electron-migration.md
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
# Electron App Migration Strategy
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document outlines the migration strategy for the TimeSafari Electron app, focusing on the transition from web-based storage to native SQLite implementation while maintaining cross-platform compatibility.
|
||||||
|
|
||||||
|
## Current Architecture
|
||||||
|
|
||||||
|
### 1. Platform Services
|
||||||
|
- `ElectronPlatformService`: Implements platform-specific features for desktop
|
||||||
|
- Uses `@capacitor-community/sqlite` for database operations
|
||||||
|
- Maintains compatibility with web/mobile platforms through shared interfaces
|
||||||
|
|
||||||
|
### 2. Database Implementation
|
||||||
|
- SQLite with native Node.js backend
|
||||||
|
- WAL journal mode for better concurrency
|
||||||
|
- Connection pooling for performance
|
||||||
|
- Migration system for schema updates
|
||||||
|
- Secure file permissions (0o755)
|
||||||
|
|
||||||
|
### 3. Build Process
|
||||||
|
```bash
|
||||||
|
# Development
|
||||||
|
npm run dev:electron
|
||||||
|
|
||||||
|
# Production Build
|
||||||
|
npm run build:web
|
||||||
|
npm run build:electron
|
||||||
|
npm run electron:build-linux # or electron:build-mac
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Goals
|
||||||
|
|
||||||
|
1. **Data Integrity**
|
||||||
|
- Preserve existing data during migration
|
||||||
|
- Maintain data relationships
|
||||||
|
- Ensure ACID compliance
|
||||||
|
- Implement proper backup/restore
|
||||||
|
|
||||||
|
2. **Performance**
|
||||||
|
- Optimize SQLite configuration
|
||||||
|
- Implement connection pooling
|
||||||
|
- Use WAL journal mode
|
||||||
|
- Configure optimal PRAGMA settings
|
||||||
|
|
||||||
|
3. **Security**
|
||||||
|
- Secure file permissions
|
||||||
|
- Proper IPC communication
|
||||||
|
- Context isolation
|
||||||
|
- Safe preload scripts
|
||||||
|
|
||||||
|
4. **User Experience**
|
||||||
|
- Zero data loss
|
||||||
|
- Automatic migration
|
||||||
|
- Progress indicators
|
||||||
|
- Error recovery
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### 1. Database Initialization
|
||||||
|
```typescript
|
||||||
|
// electron/src/rt/sqlite-init.ts
|
||||||
|
export async function initializeSQLite() {
|
||||||
|
// Set up database path with proper permissions
|
||||||
|
const dbPath = path.join(app.getPath('userData'), 'timesafari.db');
|
||||||
|
|
||||||
|
// Initialize SQLite plugin
|
||||||
|
const sqlite = new CapacitorSQLite();
|
||||||
|
|
||||||
|
// Configure database
|
||||||
|
await sqlite.createConnection({
|
||||||
|
database: 'timesafari',
|
||||||
|
path: dbPath,
|
||||||
|
encrypted: false,
|
||||||
|
mode: 'no-encryption'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set optimal PRAGMA settings
|
||||||
|
await sqlite.execute({
|
||||||
|
database: 'timesafari',
|
||||||
|
statements: [
|
||||||
|
'PRAGMA journal_mode = WAL;',
|
||||||
|
'PRAGMA synchronous = NORMAL;',
|
||||||
|
'PRAGMA foreign_keys = ON;'
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Migration System
|
||||||
|
```typescript
|
||||||
|
// electron/src/rt/sqlite-migrations.ts
|
||||||
|
interface Migration {
|
||||||
|
version: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
sql: string;
|
||||||
|
rollback?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runMigrations(plugin: any, database: string) {
|
||||||
|
// Track migration state
|
||||||
|
const state = await getMigrationState(plugin, database);
|
||||||
|
|
||||||
|
// Execute migrations in transaction
|
||||||
|
for (const migration of pendingMigrations) {
|
||||||
|
await executeMigration(plugin, database, migration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Platform Service Implementation
|
||||||
|
```typescript
|
||||||
|
// src/services/platforms/ElectronPlatformService.ts
|
||||||
|
export class ElectronPlatformService implements PlatformService {
|
||||||
|
private sqlite: any;
|
||||||
|
|
||||||
|
async dbQuery(sql: string, params: any[]): Promise<QueryExecResult> {
|
||||||
|
return await this.sqlite.execute({
|
||||||
|
database: 'timesafari',
|
||||||
|
statements: [{ statement: sql, values: params }]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Preload Script
|
||||||
|
```typescript
|
||||||
|
// electron/preload.ts
|
||||||
|
contextBridge.exposeInMainWorld('electron', {
|
||||||
|
sqlite: {
|
||||||
|
isAvailable: () => ipcRenderer.invoke('sqlite:isAvailable'),
|
||||||
|
execute: (method: string, ...args: unknown[]) =>
|
||||||
|
ipcRenderer.invoke('sqlite:execute', method, ...args)
|
||||||
|
},
|
||||||
|
getPath: (pathType: string) => ipcRenderer.invoke('get-path', pathType),
|
||||||
|
env: {
|
||||||
|
platform: 'electron'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build Configuration
|
||||||
|
|
||||||
|
### 1. Vite Configuration
|
||||||
|
```typescript
|
||||||
|
// vite.config.app.electron.mts
|
||||||
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
emptyOutDir: true
|
||||||
|
},
|
||||||
|
define: {
|
||||||
|
'process.env.VITE_PLATFORM': JSON.stringify('electron'),
|
||||||
|
'process.env.VITE_PWA_ENABLED': JSON.stringify(false)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Package Scripts
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"dev:electron": "vite build --watch --config vite.config.app.electron.mts",
|
||||||
|
"build:electron": "vite build --config vite.config.app.electron.mts",
|
||||||
|
"electron:build-linux": "electron-builder --linux",
|
||||||
|
"electron:build-mac": "electron-builder --mac"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
1. **Unit Tests**
|
||||||
|
- Database operations
|
||||||
|
- Migration system
|
||||||
|
- Platform service methods
|
||||||
|
- IPC communication
|
||||||
|
|
||||||
|
2. **Integration Tests**
|
||||||
|
- Full migration process
|
||||||
|
- Data integrity verification
|
||||||
|
- Cross-platform compatibility
|
||||||
|
- Error recovery
|
||||||
|
|
||||||
|
3. **End-to-End Tests**
|
||||||
|
- User workflows
|
||||||
|
- Data persistence
|
||||||
|
- UI interactions
|
||||||
|
- Platform-specific features
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
1. **Database Errors**
|
||||||
|
- Connection failures
|
||||||
|
- Migration errors
|
||||||
|
- Query execution errors
|
||||||
|
- Transaction failures
|
||||||
|
|
||||||
|
2. **Platform Errors**
|
||||||
|
- File system errors
|
||||||
|
- IPC communication errors
|
||||||
|
- Permission issues
|
||||||
|
- Resource constraints
|
||||||
|
|
||||||
|
3. **Recovery Mechanisms**
|
||||||
|
- Automatic retry logic
|
||||||
|
- Transaction rollback
|
||||||
|
- State verification
|
||||||
|
- User notifications
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
1. **File System**
|
||||||
|
- Secure file permissions
|
||||||
|
- Path validation
|
||||||
|
- Access control
|
||||||
|
- Data encryption
|
||||||
|
|
||||||
|
2. **IPC Communication**
|
||||||
|
- Context isolation
|
||||||
|
- Channel validation
|
||||||
|
- Data sanitization
|
||||||
|
- Error handling
|
||||||
|
|
||||||
|
3. **Preload Scripts**
|
||||||
|
- Minimal API exposure
|
||||||
|
- Type safety
|
||||||
|
- Input validation
|
||||||
|
- Error boundaries
|
||||||
|
|
||||||
|
## Future Improvements
|
||||||
|
|
||||||
|
1. **Performance**
|
||||||
|
- Query optimization
|
||||||
|
- Index tuning
|
||||||
|
- Connection management
|
||||||
|
- Cache implementation
|
||||||
|
|
||||||
|
2. **Features**
|
||||||
|
- Offline support
|
||||||
|
- Sync capabilities
|
||||||
|
- Backup/restore
|
||||||
|
- Data export/import
|
||||||
|
|
||||||
|
3. **Security**
|
||||||
|
- Database encryption
|
||||||
|
- Secure storage
|
||||||
|
- Access control
|
||||||
|
- Audit logging
|
||||||
|
|
||||||
|
## Maintenance
|
||||||
|
|
||||||
|
1. **Regular Tasks**
|
||||||
|
- Database optimization
|
||||||
|
- Log rotation
|
||||||
|
- Error monitoring
|
||||||
|
- Performance tracking
|
||||||
|
|
||||||
|
2. **Updates**
|
||||||
|
- Dependency updates
|
||||||
|
- Security patches
|
||||||
|
- Feature additions
|
||||||
|
- Bug fixes
|
||||||
|
|
||||||
|
3. **Documentation**
|
||||||
|
- API documentation
|
||||||
|
- Migration guides
|
||||||
|
- Troubleshooting
|
||||||
|
- Best practices
|
||||||
55
electron/.gitignore
vendored
Normal file
55
electron/.gitignore
vendored
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# NPM renames .gitignore to .npmignore
|
||||||
|
# In order to prevent that, we remove the initial "."
|
||||||
|
# And the CLI then renames it
|
||||||
|
app
|
||||||
|
node_modules
|
||||||
|
build
|
||||||
|
dist
|
||||||
|
logs
|
||||||
|
# Node.js dependencies
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Capacitor build outputs
|
||||||
|
web/
|
||||||
|
ios/
|
||||||
|
android/
|
||||||
|
electron/app/
|
||||||
|
|
||||||
|
# Capacitor SQLite plugin data (important!)
|
||||||
|
capacitor-sqlite/
|
||||||
|
|
||||||
|
# TypeScript / build output
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Development / IDE files
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
.vscode/
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
# JetBrains IDEs (IntelliJ, WebStorm, etc.)
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# macOS specific
|
||||||
|
.DS_Store
|
||||||
|
*.swp
|
||||||
|
*~
|
||||||
|
*.tmp
|
||||||
|
|
||||||
|
# Windows specific
|
||||||
|
Thumbs.db
|
||||||
|
ehthumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
$RECYCLE.BIN/
|
||||||
BIN
electron/assets/appIcon.ico
Normal file
BIN
electron/assets/appIcon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 142 KiB |
BIN
electron/assets/appIcon.png
Normal file
BIN
electron/assets/appIcon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
BIN
electron/assets/splash.gif
Normal file
BIN
electron/assets/splash.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 159 KiB |
BIN
electron/assets/splash.png
Normal file
BIN
electron/assets/splash.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
62
electron/capacitor.config.json
Normal file
62
electron/capacitor.config.json
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"appId": "com.timesafari.app",
|
||||||
|
"appName": "TimeSafari",
|
||||||
|
"webDir": "dist",
|
||||||
|
"bundledWebRuntime": false,
|
||||||
|
"server": {
|
||||||
|
"cleartext": true,
|
||||||
|
"androidScheme": "https"
|
||||||
|
},
|
||||||
|
"plugins": {
|
||||||
|
"App": {
|
||||||
|
"appUrlOpen": {
|
||||||
|
"handlers": [
|
||||||
|
{
|
||||||
|
"url": "timesafari://*",
|
||||||
|
"autoVerify": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SQLite": {
|
||||||
|
"iosDatabaseLocation": "Library/CapacitorDatabase",
|
||||||
|
"iosIsEncryption": true,
|
||||||
|
"iosBiometric": {
|
||||||
|
"biometricAuth": true,
|
||||||
|
"biometricTitle": "Biometric login for TimeSafari"
|
||||||
|
},
|
||||||
|
"androidIsEncryption": true,
|
||||||
|
"androidBiometric": {
|
||||||
|
"biometricAuth": true,
|
||||||
|
"biometricTitle": "Biometric login for TimeSafari"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CapacitorSQLite": {
|
||||||
|
"electronIsEncryption": false,
|
||||||
|
"electronMacLocation": "~/Library/Application Support/TimeSafari",
|
||||||
|
"electronWindowsLocation": "C:\\ProgramData\\TimeSafari"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ios": {
|
||||||
|
"contentInset": "always",
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
28
electron/electron-builder.config.json
Normal file
28
electron/electron-builder.config.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"appId": "com.yourdoamnin.yourapp",
|
||||||
|
"directories": {
|
||||||
|
"buildResources": "resources"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"assets/**/*",
|
||||||
|
"build/**/*",
|
||||||
|
"capacitor.config.*",
|
||||||
|
"app/**/*"
|
||||||
|
],
|
||||||
|
"publish": {
|
||||||
|
"provider": "github"
|
||||||
|
},
|
||||||
|
"nsis": {
|
||||||
|
"allowElevation": true,
|
||||||
|
"oneClick": false,
|
||||||
|
"allowToChangeInstallationDirectory": true
|
||||||
|
},
|
||||||
|
"win": {
|
||||||
|
"target": "nsis",
|
||||||
|
"icon": "assets/appIcon.ico"
|
||||||
|
},
|
||||||
|
"mac": {
|
||||||
|
"category": "your.app.category.type",
|
||||||
|
"target": "dmg"
|
||||||
|
}
|
||||||
|
}
|
||||||
75
electron/live-runner.js
Normal file
75
electron/live-runner.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
/* eslint-disable no-undef */
|
||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
const cp = require('child_process');
|
||||||
|
const chokidar = require('chokidar');
|
||||||
|
const electron = require('electron');
|
||||||
|
|
||||||
|
let child = null;
|
||||||
|
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
||||||
|
const reloadWatcher = {
|
||||||
|
debouncer: null,
|
||||||
|
ready: false,
|
||||||
|
watcher: null,
|
||||||
|
restarting: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
///*
|
||||||
|
function runBuild() {
|
||||||
|
return new Promise((resolve, _reject) => {
|
||||||
|
let tempChild = cp.spawn(npmCmd, ['run', 'build']);
|
||||||
|
tempChild.once('exit', () => {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
tempChild.stdout.pipe(process.stdout);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
//*/
|
||||||
|
|
||||||
|
async function spawnElectron() {
|
||||||
|
if (child !== null) {
|
||||||
|
child.stdin.pause();
|
||||||
|
child.kill();
|
||||||
|
child = null;
|
||||||
|
await runBuild();
|
||||||
|
}
|
||||||
|
child = cp.spawn(electron, ['--inspect=5858', './']);
|
||||||
|
child.on('exit', () => {
|
||||||
|
if (!reloadWatcher.restarting) {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
child.stdout.pipe(process.stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupReloadWatcher() {
|
||||||
|
reloadWatcher.watcher = chokidar
|
||||||
|
.watch('./src/**/*', {
|
||||||
|
ignored: /[/\\]\./,
|
||||||
|
persistent: true,
|
||||||
|
})
|
||||||
|
.on('ready', () => {
|
||||||
|
reloadWatcher.ready = true;
|
||||||
|
})
|
||||||
|
.on('all', (_event, _path) => {
|
||||||
|
if (reloadWatcher.ready) {
|
||||||
|
clearTimeout(reloadWatcher.debouncer);
|
||||||
|
reloadWatcher.debouncer = setTimeout(async () => {
|
||||||
|
console.log('Restarting');
|
||||||
|
reloadWatcher.restarting = true;
|
||||||
|
await spawnElectron();
|
||||||
|
reloadWatcher.restarting = false;
|
||||||
|
reloadWatcher.ready = false;
|
||||||
|
clearTimeout(reloadWatcher.debouncer);
|
||||||
|
reloadWatcher.debouncer = null;
|
||||||
|
reloadWatcher.watcher = null;
|
||||||
|
setupReloadWatcher();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
await runBuild();
|
||||||
|
await spawnElectron();
|
||||||
|
setupReloadWatcher();
|
||||||
|
})();
|
||||||
5460
electron/package-lock.json
generated
Normal file
5460
electron/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
52
electron/package.json
Normal file
52
electron/package.json
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"name": "TimeSafari",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "TimeSafari Electron App",
|
||||||
|
"author": {
|
||||||
|
"name": "",
|
||||||
|
"email": ""
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": ""
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"main": "build/src/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc && electron-rebuild",
|
||||||
|
"electron:start-live": "node ./live-runner.js",
|
||||||
|
"electron:start": "npm run build && electron --inspect=5858 ./",
|
||||||
|
"electron:pack": "npm run build && electron-builder build --dir -c ./electron-builder.config.json",
|
||||||
|
"electron:make": "npm run build && electron-builder build -c ./electron-builder.config.json -p always"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@capacitor-community/electron": "^5.0.0",
|
||||||
|
"@capacitor-community/sqlite": "^6.0.2",
|
||||||
|
"better-sqlite3-multiple-ciphers": "^11.10.0",
|
||||||
|
"chokidar": "~3.5.3",
|
||||||
|
"crypto": "^1.0.1",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
|
"electron-is-dev": "~2.0.0",
|
||||||
|
"electron-json-storage": "^4.6.0",
|
||||||
|
"electron-serve": "~1.1.0",
|
||||||
|
"electron-unhandled": "~4.0.1",
|
||||||
|
"electron-updater": "^5.3.0",
|
||||||
|
"electron-window-state": "^5.0.3",
|
||||||
|
"jszip": "^3.10.1",
|
||||||
|
"node-fetch": "^2.6.7",
|
||||||
|
"winston": "^3.17.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
|
"@types/crypto-js": "^4.2.2",
|
||||||
|
"@types/electron-json-storage": "^4.5.4",
|
||||||
|
"electron": "^26.2.2",
|
||||||
|
"electron-builder": "~23.6.0",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"typescript": "^5.0.4"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"capacitor",
|
||||||
|
"electron"
|
||||||
|
]
|
||||||
|
}
|
||||||
10
electron/resources/electron-publisher-custom.js
Normal file
10
electron/resources/electron-publisher-custom.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/* eslint-disable no-undef */
|
||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
const electronPublish = require('electron-publish');
|
||||||
|
|
||||||
|
class Publisher extends electronPublish.Publisher {
|
||||||
|
async upload(task) {
|
||||||
|
console.log('electron-publisher-custom', task.file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = Publisher;
|
||||||
140
electron/src/index.ts
Normal file
140
electron/src/index.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import type { CapacitorElectronConfig } from '@capacitor-community/electron';
|
||||||
|
import { getCapacitorElectronConfig, setupElectronDeepLinking } from '@capacitor-community/electron';
|
||||||
|
import type { MenuItemConstructorOptions } from 'electron';
|
||||||
|
import { app, MenuItem } from 'electron';
|
||||||
|
import electronIsDev from 'electron-is-dev';
|
||||||
|
import unhandled from 'electron-unhandled';
|
||||||
|
import { autoUpdater } from 'electron-updater';
|
||||||
|
|
||||||
|
import { ElectronCapacitorApp, setupContentSecurityPolicy, setupReloadWatcher } from './setup';
|
||||||
|
import { initializeSQLite, setupSQLiteHandlers } from './rt/sqlite-init';
|
||||||
|
|
||||||
|
// Graceful handling of unhandled errors.
|
||||||
|
unhandled();
|
||||||
|
|
||||||
|
// Define our menu templates (these are optional)
|
||||||
|
const trayMenuTemplate: (MenuItemConstructorOptions | MenuItem)[] = [new MenuItem({ label: 'Quit App', role: 'quit' })];
|
||||||
|
const appMenuBarMenuTemplate: (MenuItemConstructorOptions | MenuItem)[] = [
|
||||||
|
{ role: process.platform === 'darwin' ? 'appMenu' : 'fileMenu' },
|
||||||
|
{ role: 'viewMenu' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Get Config options from capacitor.config
|
||||||
|
const capacitorFileConfig: CapacitorElectronConfig = getCapacitorElectronConfig();
|
||||||
|
|
||||||
|
// Initialize our app. You can pass menu templates into the app here.
|
||||||
|
const myCapacitorApp = new ElectronCapacitorApp(capacitorFileConfig, trayMenuTemplate, appMenuBarMenuTemplate);
|
||||||
|
|
||||||
|
// If deeplinking is enabled then we will set it up here.
|
||||||
|
if (capacitorFileConfig.electron?.deepLinkingEnabled) {
|
||||||
|
setupElectronDeepLinking(myCapacitorApp, {
|
||||||
|
customProtocol: capacitorFileConfig.electron.deepLinkingCustomProtocol ?? 'mycapacitorapp',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we are in Dev mode, use the file watcher components.
|
||||||
|
if (electronIsDev) {
|
||||||
|
setupReloadWatcher(myCapacitorApp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run Application
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
// Wait for electron app to be ready first
|
||||||
|
await app.whenReady();
|
||||||
|
console.log('[Electron Main Process] App is ready');
|
||||||
|
|
||||||
|
// Initialize SQLite plugin and handlers BEFORE creating any windows
|
||||||
|
console.log('[Electron Main Process] Initializing SQLite...');
|
||||||
|
setupSQLiteHandlers();
|
||||||
|
await initializeSQLite();
|
||||||
|
console.log('[Electron Main Process] SQLite initialization complete');
|
||||||
|
|
||||||
|
// Security - Set Content-Security-Policy
|
||||||
|
setupContentSecurityPolicy(myCapacitorApp.getCustomURLScheme());
|
||||||
|
|
||||||
|
// Initialize our app and create window
|
||||||
|
console.log('[Electron Main Process] Starting app initialization...');
|
||||||
|
await myCapacitorApp.init();
|
||||||
|
console.log('[Electron Main Process] App initialization complete');
|
||||||
|
|
||||||
|
// Get the main window
|
||||||
|
const mainWindow = myCapacitorApp.getMainWindow();
|
||||||
|
if (!mainWindow) {
|
||||||
|
throw new Error('Main window not available after app initialization');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for window to be ready and loaded
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
const handleReady = () => {
|
||||||
|
console.log('[Electron Main Process] Window ready to show');
|
||||||
|
mainWindow.show();
|
||||||
|
|
||||||
|
// Wait for window to finish loading
|
||||||
|
mainWindow.webContents.once('did-finish-load', () => {
|
||||||
|
console.log('[Electron Main Process] Window finished loading');
|
||||||
|
|
||||||
|
// Send SQLite ready signal after window is fully loaded
|
||||||
|
if (!mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('sqlite-ready');
|
||||||
|
console.log('[Electron Main Process] Sent SQLite ready signal to renderer');
|
||||||
|
} else {
|
||||||
|
console.warn('[Electron Main Process] Window was destroyed before sending SQLite ready signal');
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Always use the event since isReadyToShow is not reliable
|
||||||
|
mainWindow.once('ready-to-show', handleReady);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check for updates if we are in a packaged app
|
||||||
|
if (!electronIsDev) {
|
||||||
|
console.log('[Electron Main Process] Checking for updates...');
|
||||||
|
autoUpdater.checkForUpdatesAndNotify();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle window close
|
||||||
|
mainWindow.on('closed', () => {
|
||||||
|
console.log('[Electron Main Process] Main window closed');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle window close request
|
||||||
|
mainWindow.on('close', (event) => {
|
||||||
|
console.log('[Electron Main Process] Window close requested');
|
||||||
|
if (mainWindow.webContents.isLoading()) {
|
||||||
|
event.preventDefault();
|
||||||
|
console.log('[Electron Main Process] Deferring window close due to loading state');
|
||||||
|
mainWindow.webContents.once('did-finish-load', () => {
|
||||||
|
mainWindow.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Electron Main Process] Fatal error during initialization:', error);
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Handle when all of our windows are close (platforms have their own expectations).
|
||||||
|
app.on('window-all-closed', function () {
|
||||||
|
// On OS X it is common for applications and their menu bar
|
||||||
|
// to stay active until the user quits explicitly with Cmd + Q
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// When the dock icon is clicked.
|
||||||
|
app.on('activate', async function () {
|
||||||
|
// On OS X it's common to re-create a window in the app when the
|
||||||
|
// dock icon is clicked and there are no other windows open.
|
||||||
|
if (myCapacitorApp.getMainWindow().isDestroyed()) {
|
||||||
|
await myCapacitorApp.init();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Place all ipc or other electron api calls and custom functionality under this line
|
||||||
303
electron/src/preload.ts
Normal file
303
electron/src/preload.ts
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
/**
|
||||||
|
* Preload script for Electron
|
||||||
|
* Sets up secure IPC communication between renderer and main process
|
||||||
|
*
|
||||||
|
* @author Matthew Raymer
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { contextBridge, ipcRenderer } from 'electron';
|
||||||
|
|
||||||
|
// Enhanced logger for preload script that forwards to main process
|
||||||
|
const logger = {
|
||||||
|
log: (...args: unknown[]) => {
|
||||||
|
console.log('[Preload]', ...args);
|
||||||
|
ipcRenderer.send('renderer-log', { level: 'log', args });
|
||||||
|
},
|
||||||
|
error: (...args: unknown[]) => {
|
||||||
|
console.error('[Preload]', ...args);
|
||||||
|
ipcRenderer.send('renderer-log', { level: 'error', args });
|
||||||
|
},
|
||||||
|
info: (...args: unknown[]) => {
|
||||||
|
console.info('[Preload]', ...args);
|
||||||
|
ipcRenderer.send('renderer-log', { level: 'info', args });
|
||||||
|
},
|
||||||
|
warn: (...args: unknown[]) => {
|
||||||
|
console.warn('[Preload]', ...args);
|
||||||
|
ipcRenderer.send('renderer-log', { level: 'warn', args });
|
||||||
|
},
|
||||||
|
debug: (...args: unknown[]) => {
|
||||||
|
console.debug('[Preload]', ...args);
|
||||||
|
ipcRenderer.send('renderer-log', { level: 'debug', args });
|
||||||
|
},
|
||||||
|
sqlite: {
|
||||||
|
log: (operation: string, ...args: unknown[]) => {
|
||||||
|
const message = ['[Preload][SQLite]', operation, ...args];
|
||||||
|
console.log(...message);
|
||||||
|
ipcRenderer.send('renderer-log', {
|
||||||
|
level: 'log',
|
||||||
|
args: message,
|
||||||
|
source: 'sqlite',
|
||||||
|
operation
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: (operation: string, error: unknown) => {
|
||||||
|
const message = ['[Preload][SQLite]', operation, 'failed:', error];
|
||||||
|
console.error(...message);
|
||||||
|
ipcRenderer.send('renderer-log', {
|
||||||
|
level: 'error',
|
||||||
|
args: message,
|
||||||
|
source: 'sqlite',
|
||||||
|
operation,
|
||||||
|
error: error instanceof Error ? {
|
||||||
|
name: error.name,
|
||||||
|
message: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
} : error
|
||||||
|
});
|
||||||
|
},
|
||||||
|
debug: (operation: string, ...args: unknown[]) => {
|
||||||
|
const message = ['[Preload][SQLite]', operation, ...args];
|
||||||
|
console.debug(...message);
|
||||||
|
ipcRenderer.send('renderer-log', {
|
||||||
|
level: 'debug',
|
||||||
|
args: message,
|
||||||
|
source: 'sqlite',
|
||||||
|
operation
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Types for SQLite connection options
|
||||||
|
interface SQLiteConnectionOptions {
|
||||||
|
database: string;
|
||||||
|
version?: number;
|
||||||
|
readOnly?: boolean;
|
||||||
|
readonly?: boolean; // Handle both cases
|
||||||
|
encryption?: string;
|
||||||
|
mode?: string;
|
||||||
|
useNative?: boolean;
|
||||||
|
[key: string]: unknown; // Allow other properties
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define valid channels for security
|
||||||
|
const VALID_CHANNELS = {
|
||||||
|
send: ['toMain'] as const,
|
||||||
|
receive: ['fromMain', 'sqlite-ready', 'database-status'] as const,
|
||||||
|
invoke: [
|
||||||
|
'sqlite-is-available',
|
||||||
|
'sqlite-echo',
|
||||||
|
'sqlite-create-connection',
|
||||||
|
'sqlite-execute',
|
||||||
|
'sqlite-query',
|
||||||
|
'sqlite-run',
|
||||||
|
'sqlite-close-connection',
|
||||||
|
'sqlite-open',
|
||||||
|
'sqlite-close',
|
||||||
|
'sqlite-is-db-open',
|
||||||
|
'sqlite-status',
|
||||||
|
'get-path',
|
||||||
|
'get-base-path'
|
||||||
|
] as const
|
||||||
|
};
|
||||||
|
|
||||||
|
type ValidSendChannel = typeof VALID_CHANNELS.send[number];
|
||||||
|
type ValidReceiveChannel = typeof VALID_CHANNELS.receive[number];
|
||||||
|
type ValidInvokeChannel = typeof VALID_CHANNELS.invoke[number];
|
||||||
|
|
||||||
|
// Create a secure IPC bridge
|
||||||
|
const createSecureIPCBridge = () => {
|
||||||
|
return {
|
||||||
|
send: (channel: string, data: unknown) => {
|
||||||
|
if (VALID_CHANNELS.send.includes(channel as ValidSendChannel)) {
|
||||||
|
logger.debug('IPC Send:', channel, data);
|
||||||
|
ipcRenderer.send(channel, data);
|
||||||
|
} else {
|
||||||
|
logger.warn(`[Preload] Attempted to send on invalid channel: ${channel}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
receive: (channel: string, func: (...args: unknown[]) => void) => {
|
||||||
|
if (VALID_CHANNELS.receive.includes(channel as ValidReceiveChannel)) {
|
||||||
|
logger.debug('IPC Receive:', channel);
|
||||||
|
ipcRenderer.on(channel, (_event, ...args) => {
|
||||||
|
logger.debug('IPC Received:', channel, args);
|
||||||
|
func(...args);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.warn(`[Preload] Attempted to receive on invalid channel: ${channel}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
once: (channel: string, func: (...args: unknown[]) => void) => {
|
||||||
|
if (VALID_CHANNELS.receive.includes(channel as ValidReceiveChannel)) {
|
||||||
|
logger.debug('IPC Once:', channel);
|
||||||
|
ipcRenderer.once(channel, (_event, ...args) => {
|
||||||
|
logger.debug('IPC Received Once:', channel, args);
|
||||||
|
func(...args);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.warn(`[Preload] Attempted to receive once on invalid channel: ${channel}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
invoke: async (channel: string, ...args: unknown[]) => {
|
||||||
|
if (VALID_CHANNELS.invoke.includes(channel as ValidInvokeChannel)) {
|
||||||
|
logger.debug('IPC Invoke:', channel, args);
|
||||||
|
try {
|
||||||
|
const result = await ipcRenderer.invoke(channel, ...args);
|
||||||
|
logger.debug('IPC Invoke Result:', channel, result);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('IPC Invoke Error:', channel, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn(`[Preload] Attempted to invoke on invalid channel: ${channel}`);
|
||||||
|
throw new Error(`Invalid channel: ${channel}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create SQLite proxy with retry logic
|
||||||
|
const createSQLiteProxy = () => {
|
||||||
|
const MAX_RETRIES = 3;
|
||||||
|
const RETRY_DELAY = 1000;
|
||||||
|
|
||||||
|
const withRetry = async <T>(operation: string, ...args: unknown[]): Promise<T> => {
|
||||||
|
let lastError: Error | undefined;
|
||||||
|
const operationId = `${operation}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
logger.sqlite.debug(operation, 'starting with args:', {
|
||||||
|
operationId,
|
||||||
|
args,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||||
|
try {
|
||||||
|
logger.sqlite.debug(operation, `attempt ${attempt}/${MAX_RETRIES}`, {
|
||||||
|
operationId,
|
||||||
|
attempt,
|
||||||
|
args,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log the exact IPC call
|
||||||
|
logger.sqlite.debug(operation, 'invoking IPC', {
|
||||||
|
operationId,
|
||||||
|
channel: `sqlite-${operation}`,
|
||||||
|
args,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await ipcRenderer.invoke(`sqlite-${operation}`, ...args);
|
||||||
|
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
logger.sqlite.log(operation, 'success', {
|
||||||
|
operationId,
|
||||||
|
attempt,
|
||||||
|
result,
|
||||||
|
duration: `${duration}ms`,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
return result as T;
|
||||||
|
} catch (error) {
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
lastError = error instanceof Error ? error : new Error(String(error));
|
||||||
|
|
||||||
|
logger.sqlite.error(operation, {
|
||||||
|
operationId,
|
||||||
|
attempt,
|
||||||
|
error: {
|
||||||
|
name: lastError.name,
|
||||||
|
message: lastError.message,
|
||||||
|
stack: lastError.stack
|
||||||
|
},
|
||||||
|
args,
|
||||||
|
duration: `${duration}ms`,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (attempt < MAX_RETRIES) {
|
||||||
|
const backoffDelay = RETRY_DELAY * Math.pow(2, attempt - 1);
|
||||||
|
logger.warn(`[Preload] SQLite ${operation} failed (attempt ${attempt}/${MAX_RETRIES}), retrying in ${backoffDelay}ms...`, {
|
||||||
|
operationId,
|
||||||
|
error: lastError,
|
||||||
|
args,
|
||||||
|
nextAttemptIn: `${backoffDelay}ms`,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
await new Promise(resolve => setTimeout(resolve, backoffDelay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalError = new Error(
|
||||||
|
`SQLite ${operation} failed after ${MAX_RETRIES} attempts: ${lastError?.message || "Unknown error"}`
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.error('[Preload] SQLite operation failed permanently:', {
|
||||||
|
operation,
|
||||||
|
operationId,
|
||||||
|
error: {
|
||||||
|
name: finalError.name,
|
||||||
|
message: finalError.message,
|
||||||
|
stack: finalError.stack,
|
||||||
|
originalError: lastError
|
||||||
|
},
|
||||||
|
args,
|
||||||
|
attempts: MAX_RETRIES,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
throw finalError;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
isAvailable: () => withRetry('is-available'),
|
||||||
|
echo: (value: string) => withRetry('echo', { value }),
|
||||||
|
createConnection: (options: SQLiteConnectionOptions) => withRetry('create-connection', options),
|
||||||
|
closeConnection: (options: { database: string }) => withRetry('close-connection', options),
|
||||||
|
query: (options: { statement: string; values?: unknown[] }) => withRetry('query', options),
|
||||||
|
run: (options: { statement: string; values?: unknown[] }) => withRetry('run', options),
|
||||||
|
execute: (options: { statements: { statement: string; values?: unknown[] }[] }) => withRetry('execute', options),
|
||||||
|
getPlatform: () => Promise.resolve('electron')
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Expose the secure IPC bridge and SQLite proxy
|
||||||
|
const electronAPI = {
|
||||||
|
ipcRenderer: createSecureIPCBridge(),
|
||||||
|
sqlite: createSQLiteProxy(),
|
||||||
|
env: {
|
||||||
|
platform: 'electron',
|
||||||
|
isDev: process.env.NODE_ENV === 'development'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Log the exposed API for debugging
|
||||||
|
logger.debug('Exposing Electron API:', {
|
||||||
|
hasIpcRenderer: !!electronAPI.ipcRenderer,
|
||||||
|
hasSqlite: !!electronAPI.sqlite,
|
||||||
|
sqliteMethods: Object.keys(electronAPI.sqlite),
|
||||||
|
env: electronAPI.env
|
||||||
|
});
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('electron', electronAPI);
|
||||||
|
logger.info('[Preload] IPC bridge and SQLite proxy initialized successfully');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[Preload] Failed to initialize IPC bridge:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log startup
|
||||||
|
logger.log('[CapacitorSQLite] Preload script starting...');
|
||||||
|
|
||||||
|
// Handle window load
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
logger.log('[CapacitorSQLite] Preload script complete');
|
||||||
|
});
|
||||||
6
electron/src/rt/electron-plugins.js
Normal file
6
electron/src/rt/electron-plugins.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
const CapacitorCommunitySqlite = require('../../../node_modules/@capacitor-community/sqlite/electron/dist/plugin.js');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
CapacitorCommunitySqlite,
|
||||||
|
}
|
||||||
88
electron/src/rt/electron-rt.ts
Normal file
88
electron/src/rt/electron-rt.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { randomBytes } from 'crypto';
|
||||||
|
import { ipcRenderer, contextBridge } from 'electron';
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const plugins = require('./electron-plugins');
|
||||||
|
|
||||||
|
const randomId = (length = 5) => randomBytes(length).toString('hex');
|
||||||
|
|
||||||
|
const contextApi: {
|
||||||
|
[plugin: string]: { [functionName: string]: () => Promise<any> };
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
Object.keys(plugins).forEach((pluginKey) => {
|
||||||
|
Object.keys(plugins[pluginKey])
|
||||||
|
.filter((className) => className !== 'default')
|
||||||
|
.forEach((classKey) => {
|
||||||
|
const functionList = Object.getOwnPropertyNames(plugins[pluginKey][classKey].prototype).filter(
|
||||||
|
(v) => v !== 'constructor'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!contextApi[classKey]) {
|
||||||
|
contextApi[classKey] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
functionList.forEach((functionName) => {
|
||||||
|
if (!contextApi[classKey][functionName]) {
|
||||||
|
contextApi[classKey][functionName] = (...args) => ipcRenderer.invoke(`${classKey}-${functionName}`, ...args);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Events
|
||||||
|
if (plugins[pluginKey][classKey].prototype instanceof EventEmitter) {
|
||||||
|
const listeners: { [key: string]: { type: string; listener: (...args: any[]) => void } } = {};
|
||||||
|
const listenersOfTypeExist = (type) =>
|
||||||
|
!!Object.values(listeners).find((listenerObj) => listenerObj.type === type);
|
||||||
|
|
||||||
|
Object.assign(contextApi[classKey], {
|
||||||
|
addListener(type: string, callback: (...args) => void) {
|
||||||
|
const id = randomId();
|
||||||
|
|
||||||
|
// Deduplicate events
|
||||||
|
if (!listenersOfTypeExist(type)) {
|
||||||
|
ipcRenderer.send(`event-add-${classKey}`, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventHandler = (_, ...args) => callback(...args);
|
||||||
|
|
||||||
|
ipcRenderer.addListener(`event-${classKey}-${type}`, eventHandler);
|
||||||
|
listeners[id] = { type, listener: eventHandler };
|
||||||
|
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
removeListener(id: string) {
|
||||||
|
if (!listeners[id]) {
|
||||||
|
throw new Error('Invalid id');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { type, listener } = listeners[id];
|
||||||
|
|
||||||
|
ipcRenderer.removeListener(`event-${classKey}-${type}`, listener);
|
||||||
|
|
||||||
|
delete listeners[id];
|
||||||
|
|
||||||
|
if (!listenersOfTypeExist(type)) {
|
||||||
|
ipcRenderer.send(`event-remove-${classKey}-${type}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeAllListeners(type: string) {
|
||||||
|
Object.entries(listeners).forEach(([id, listenerObj]) => {
|
||||||
|
if (!type || listenerObj.type === type) {
|
||||||
|
ipcRenderer.removeListener(`event-${classKey}-${listenerObj.type}`, listenerObj.listener);
|
||||||
|
ipcRenderer.send(`event-remove-${classKey}-${listenerObj.type}`);
|
||||||
|
delete listeners[id];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('CapacitorCustomPlatform', {
|
||||||
|
name: 'electron',
|
||||||
|
plugins: contextApi,
|
||||||
|
});
|
||||||
|
////////////////////////////////////////////////////////
|
||||||
188
electron/src/rt/logger.ts
Normal file
188
electron/src/rt/logger.ts
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
/**
|
||||||
|
* Enhanced logging system for TimeSafari Electron
|
||||||
|
* Provides structured logging with proper levels and formatting
|
||||||
|
* Supports both console and file output with different verbosity levels
|
||||||
|
*
|
||||||
|
* @author Matthew Raymer
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { app, ipcMain } from 'electron';
|
||||||
|
import winston from 'winston';
|
||||||
|
import path from 'path';
|
||||||
|
import os from 'os';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
// Extend Winston Logger type with our custom loggers
|
||||||
|
declare module 'winston' {
|
||||||
|
interface Logger {
|
||||||
|
sqlite: {
|
||||||
|
debug: (message: string, ...args: unknown[]) => void;
|
||||||
|
info: (message: string, ...args: unknown[]) => void;
|
||||||
|
warn: (message: string, ...args: unknown[]) => void;
|
||||||
|
error: (message: string, ...args: unknown[]) => void;
|
||||||
|
};
|
||||||
|
migration: {
|
||||||
|
debug: (message: string, ...args: unknown[]) => void;
|
||||||
|
info: (message: string, ...args: unknown[]) => void;
|
||||||
|
warn: (message: string, ...args: unknown[]) => void;
|
||||||
|
error: (message: string, ...args: unknown[]) => void;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create logs directory if it doesn't exist
|
||||||
|
const logsDir = path.join(app.getPath('userData'), 'logs');
|
||||||
|
if (!fs.existsSync(logsDir)) {
|
||||||
|
fs.mkdirSync(logsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom format for console output with migration filtering
|
||||||
|
const consoleFormat = winston.format.combine(
|
||||||
|
winston.format.timestamp(),
|
||||||
|
winston.format.colorize(),
|
||||||
|
winston.format.printf(({ level, message, timestamp, ...metadata }) => {
|
||||||
|
// Skip migration logs unless DEBUG_MIGRATIONS is set
|
||||||
|
if (level === 'info' &&
|
||||||
|
typeof message === 'string' &&
|
||||||
|
message.includes('[Migration]') &&
|
||||||
|
!process.env.DEBUG_MIGRATIONS) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let msg = `${timestamp} [${level}] ${message}`;
|
||||||
|
if (Object.keys(metadata).length > 0) {
|
||||||
|
msg += ` ${JSON.stringify(metadata, null, 2)}`;
|
||||||
|
}
|
||||||
|
return msg;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Custom format for file output
|
||||||
|
const fileFormat = winston.format.combine(
|
||||||
|
winston.format.timestamp(),
|
||||||
|
winston.format.json()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create logger instance
|
||||||
|
const logger = winston.createLogger({
|
||||||
|
level: process.env.NODE_ENV === 'development' ? 'debug' : 'info',
|
||||||
|
format: fileFormat,
|
||||||
|
defaultMeta: { service: 'timesafari-electron' },
|
||||||
|
transports: [
|
||||||
|
// Console transport with custom format and migration filtering
|
||||||
|
new winston.transports.Console({
|
||||||
|
format: consoleFormat,
|
||||||
|
level: process.env.NODE_ENV === 'development' ? 'debug' : 'info',
|
||||||
|
silent: false // Ensure we can still see non-migration logs
|
||||||
|
}),
|
||||||
|
// File transport for all logs
|
||||||
|
new winston.transports.File({
|
||||||
|
filename: path.join(logsDir, 'error.log'),
|
||||||
|
level: 'error',
|
||||||
|
maxsize: 5242880, // 5MB
|
||||||
|
maxFiles: 5
|
||||||
|
}),
|
||||||
|
// File transport for all logs including debug
|
||||||
|
new winston.transports.File({
|
||||||
|
filename: path.join(logsDir, 'combined.log'),
|
||||||
|
maxsize: 5242880, // 5MB
|
||||||
|
maxFiles: 5
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}) as winston.Logger & {
|
||||||
|
sqlite: {
|
||||||
|
debug: (message: string, ...args: unknown[]) => void;
|
||||||
|
info: (message: string, ...args: unknown[]) => void;
|
||||||
|
warn: (message: string, ...args: unknown[]) => void;
|
||||||
|
error: (message: string, ...args: unknown[]) => void;
|
||||||
|
};
|
||||||
|
migration: {
|
||||||
|
debug: (message: string, ...args: unknown[]) => void;
|
||||||
|
info: (message: string, ...args: unknown[]) => void;
|
||||||
|
warn: (message: string, ...args: unknown[]) => void;
|
||||||
|
error: (message: string, ...args: unknown[]) => void;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add SQLite specific logger
|
||||||
|
logger.sqlite = {
|
||||||
|
debug: (message: string, ...args: unknown[]) => {
|
||||||
|
logger.debug(`[SQLite] ${message}`, ...args);
|
||||||
|
},
|
||||||
|
info: (message: string, ...args: unknown[]) => {
|
||||||
|
logger.info(`[SQLite] ${message}`, ...args);
|
||||||
|
},
|
||||||
|
warn: (message: string, ...args: unknown[]) => {
|
||||||
|
logger.warn(`[SQLite] ${message}`, ...args);
|
||||||
|
},
|
||||||
|
error: (message: string, ...args: unknown[]) => {
|
||||||
|
logger.error(`[SQLite] ${message}`, ...args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add migration specific logger with debug filtering
|
||||||
|
logger.migration = {
|
||||||
|
debug: (message: string, ...args: unknown[]) => {
|
||||||
|
if (process.env.DEBUG_MIGRATIONS) {
|
||||||
|
//logger.debug(`[Migration] ${message}`, ...args);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
info: (message: string, ...args: unknown[]) => {
|
||||||
|
// Always log to file, but only log to console if DEBUG_MIGRATIONS is set
|
||||||
|
if (process.env.DEBUG_MIGRATIONS) {
|
||||||
|
//logger.info(`[Migration] ${message}`, ...args);
|
||||||
|
} else {
|
||||||
|
// Use a separate transport for migration logs to file only
|
||||||
|
const metadata = args[0] as Record<string, unknown>;
|
||||||
|
logger.write({
|
||||||
|
level: 'info',
|
||||||
|
message: `[Migration] ${message}`,
|
||||||
|
...(metadata || {})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
warn: (message: string, ...args: unknown[]) => {
|
||||||
|
// Always log warnings to both console and file
|
||||||
|
//logger.warn(`[Migration] ${message}`, ...args);
|
||||||
|
},
|
||||||
|
error: (message: string, ...args: unknown[]) => {
|
||||||
|
// Always log errors to both console and file
|
||||||
|
//logger.error(`[Migration] ${message}`, ...args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add renderer log handler
|
||||||
|
ipcMain.on('renderer-log', (_event, { level, args, source, operation, error }) => {
|
||||||
|
const message = args.map((arg: unknown) =>
|
||||||
|
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
|
||||||
|
).join(' ');
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
source: source || 'renderer',
|
||||||
|
...(operation && { operation }),
|
||||||
|
...(error && { error })
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (level) {
|
||||||
|
case 'error':
|
||||||
|
logger.error(message, meta);
|
||||||
|
break;
|
||||||
|
case 'warn':
|
||||||
|
logger.warn(message, meta);
|
||||||
|
break;
|
||||||
|
case 'info':
|
||||||
|
logger.info(message, meta);
|
||||||
|
break;
|
||||||
|
case 'debug':
|
||||||
|
logger.debug(message, meta);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
logger.log(level, message, meta);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export logger instance
|
||||||
|
export { logger };
|
||||||
|
|
||||||
|
// Export a function to get the logs directory
|
||||||
|
export const getLogsDirectory = () => logsDir;
|
||||||
14
electron/src/rt/sqlite-error.ts
Normal file
14
electron/src/rt/sqlite-error.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* Custom error class for SQLite operations
|
||||||
|
* Provides additional context and error tracking for SQLite operations
|
||||||
|
*/
|
||||||
|
export class SQLiteError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public operation: string,
|
||||||
|
public cause?: unknown
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'SQLiteError';
|
||||||
|
}
|
||||||
|
}
|
||||||
1147
electron/src/rt/sqlite-init.ts
Normal file
1147
electron/src/rt/sqlite-init.ts
Normal file
File diff suppressed because it is too large
Load Diff
1261
electron/src/rt/sqlite-migrations.ts
Normal file
1261
electron/src/rt/sqlite-migrations.ts
Normal file
File diff suppressed because it is too large
Load Diff
442
electron/src/setup.ts
Normal file
442
electron/src/setup.ts
Normal file
@@ -0,0 +1,442 @@
|
|||||||
|
import type { CapacitorElectronConfig } from '@capacitor-community/electron';
|
||||||
|
import {
|
||||||
|
CapElectronEventEmitter,
|
||||||
|
CapacitorSplashScreen,
|
||||||
|
setupCapacitorElectronPlugins,
|
||||||
|
} from '@capacitor-community/electron';
|
||||||
|
import chokidar from 'chokidar';
|
||||||
|
import type { MenuItemConstructorOptions } from 'electron';
|
||||||
|
import { app, BrowserWindow, Menu, MenuItem, nativeImage, Tray, session } from 'electron';
|
||||||
|
import electronIsDev from 'electron-is-dev';
|
||||||
|
import electronServe from 'electron-serve';
|
||||||
|
import windowStateKeeper from 'electron-window-state';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload watcher configuration and state management
|
||||||
|
* Prevents infinite reload loops and implements rate limiting
|
||||||
|
* Also prevents reloads during critical database operations
|
||||||
|
*
|
||||||
|
* @author Matthew Raymer
|
||||||
|
*/
|
||||||
|
const RELOAD_CONFIG = {
|
||||||
|
DEBOUNCE_MS: 1500,
|
||||||
|
COOLDOWN_MS: 5000,
|
||||||
|
MAX_RELOADS_PER_MINUTE: 10,
|
||||||
|
MAX_RELOADS_PER_SESSION: 100,
|
||||||
|
DATABASE_OPERATION_TIMEOUT_MS: 10000 // 10 second timeout for database operations
|
||||||
|
};
|
||||||
|
|
||||||
|
// Track database operation state
|
||||||
|
let isDatabaseOperationInProgress = false;
|
||||||
|
let lastDatabaseOperationTime = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a database operation is in progress or recently completed
|
||||||
|
* @returns {boolean} Whether a database operation is active
|
||||||
|
*/
|
||||||
|
const isDatabaseOperationActive = (): boolean => {
|
||||||
|
const now = Date.now();
|
||||||
|
return isDatabaseOperationInProgress ||
|
||||||
|
(now - lastDatabaseOperationTime < RELOAD_CONFIG.DATABASE_OPERATION_TIMEOUT_MS);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks the start of a database operation
|
||||||
|
*/
|
||||||
|
export const startDatabaseOperation = (): void => {
|
||||||
|
isDatabaseOperationInProgress = true;
|
||||||
|
lastDatabaseOperationTime = Date.now();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks the end of a database operation
|
||||||
|
*/
|
||||||
|
export const endDatabaseOperation = (): void => {
|
||||||
|
isDatabaseOperationInProgress = false;
|
||||||
|
lastDatabaseOperationTime = Date.now();
|
||||||
|
};
|
||||||
|
|
||||||
|
const reloadWatcher = {
|
||||||
|
debouncer: null as NodeJS.Timeout | null,
|
||||||
|
ready: false,
|
||||||
|
watcher: null as chokidar.FSWatcher | null,
|
||||||
|
lastReloadTime: 0,
|
||||||
|
reloadCount: 0,
|
||||||
|
sessionReloadCount: 0,
|
||||||
|
resetTimeout: null as NodeJS.Timeout | null,
|
||||||
|
isReloading: false
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the reload counter after one minute
|
||||||
|
*/
|
||||||
|
const resetReloadCounter = () => {
|
||||||
|
reloadWatcher.reloadCount = 0;
|
||||||
|
reloadWatcher.resetTimeout = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a reload is allowed based on rate limits, cooldown, and database state
|
||||||
|
* @returns {boolean} Whether a reload is allowed
|
||||||
|
*/
|
||||||
|
const canReload = (): boolean => {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// Check if database operation is active
|
||||||
|
if (isDatabaseOperationActive()) {
|
||||||
|
console.warn('[Reload Watcher] Skipping reload - database operation in progress');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check cooldown period
|
||||||
|
if (now - reloadWatcher.lastReloadTime < RELOAD_CONFIG.COOLDOWN_MS) {
|
||||||
|
console.warn('[Reload Watcher] Skipping reload - cooldown period active');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check per-minute limit
|
||||||
|
if (reloadWatcher.reloadCount >= RELOAD_CONFIG.MAX_RELOADS_PER_MINUTE) {
|
||||||
|
console.warn('[Reload Watcher] Skipping reload - maximum reloads per minute reached');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check session limit
|
||||||
|
if (reloadWatcher.sessionReloadCount >= RELOAD_CONFIG.MAX_RELOADS_PER_SESSION) {
|
||||||
|
console.error('[Reload Watcher] Maximum reloads per session reached. Please restart the application.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans up the current watcher instance
|
||||||
|
*/
|
||||||
|
const cleanupWatcher = () => {
|
||||||
|
if (reloadWatcher.watcher) {
|
||||||
|
reloadWatcher.watcher.close();
|
||||||
|
reloadWatcher.watcher = null;
|
||||||
|
}
|
||||||
|
if (reloadWatcher.debouncer) {
|
||||||
|
clearTimeout(reloadWatcher.debouncer);
|
||||||
|
reloadWatcher.debouncer = null;
|
||||||
|
}
|
||||||
|
if (reloadWatcher.resetTimeout) {
|
||||||
|
clearTimeout(reloadWatcher.resetTimeout);
|
||||||
|
reloadWatcher.resetTimeout = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up the file watcher for development mode reloading
|
||||||
|
* Implements rate limiting and prevents infinite reload loops
|
||||||
|
*
|
||||||
|
* @param electronCapacitorApp - The Electron Capacitor app instance
|
||||||
|
*/
|
||||||
|
export function setupReloadWatcher(electronCapacitorApp: ElectronCapacitorApp): void {
|
||||||
|
// Cleanup any existing watcher
|
||||||
|
cleanupWatcher();
|
||||||
|
|
||||||
|
// Reset state
|
||||||
|
reloadWatcher.ready = false;
|
||||||
|
reloadWatcher.isReloading = false;
|
||||||
|
|
||||||
|
reloadWatcher.watcher = chokidar
|
||||||
|
.watch(join(app.getAppPath(), 'app'), {
|
||||||
|
ignored: /[/\\]\./,
|
||||||
|
persistent: true,
|
||||||
|
awaitWriteFinish: {
|
||||||
|
stabilityThreshold: 1000,
|
||||||
|
pollInterval: 100
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('ready', () => {
|
||||||
|
reloadWatcher.ready = true;
|
||||||
|
console.log('[Reload Watcher] Ready to watch for changes');
|
||||||
|
})
|
||||||
|
.on('all', (_event, _path) => {
|
||||||
|
if (!reloadWatcher.ready || reloadWatcher.isReloading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear existing debouncer
|
||||||
|
if (reloadWatcher.debouncer) {
|
||||||
|
clearTimeout(reloadWatcher.debouncer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up new debouncer
|
||||||
|
reloadWatcher.debouncer = setTimeout(async () => {
|
||||||
|
if (!canReload()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
reloadWatcher.isReloading = true;
|
||||||
|
|
||||||
|
// Update reload counters
|
||||||
|
reloadWatcher.lastReloadTime = Date.now();
|
||||||
|
reloadWatcher.reloadCount++;
|
||||||
|
reloadWatcher.sessionReloadCount++;
|
||||||
|
|
||||||
|
// Set up reset timeout for per-minute counter
|
||||||
|
if (!reloadWatcher.resetTimeout) {
|
||||||
|
reloadWatcher.resetTimeout = setTimeout(resetReloadCounter, 60000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform reload
|
||||||
|
console.log('[Reload Watcher] Reloading window...');
|
||||||
|
await electronCapacitorApp.getMainWindow().webContents.reload();
|
||||||
|
|
||||||
|
// Reset state after reload
|
||||||
|
reloadWatcher.ready = false;
|
||||||
|
reloadWatcher.isReloading = false;
|
||||||
|
|
||||||
|
// Re-setup watcher after successful reload
|
||||||
|
setupReloadWatcher(electronCapacitorApp);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Reload Watcher] Error during reload:', error);
|
||||||
|
reloadWatcher.isReloading = false;
|
||||||
|
reloadWatcher.ready = true;
|
||||||
|
}
|
||||||
|
}, RELOAD_CONFIG.DEBOUNCE_MS);
|
||||||
|
})
|
||||||
|
.on('error', (error) => {
|
||||||
|
console.error('[Reload Watcher] Error:', error);
|
||||||
|
cleanupWatcher();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define our class to manage our app.
|
||||||
|
export class ElectronCapacitorApp {
|
||||||
|
private MainWindow: BrowserWindow | null = null;
|
||||||
|
private SplashScreen: CapacitorSplashScreen | null = null;
|
||||||
|
private TrayIcon: Tray | null = null;
|
||||||
|
private CapacitorFileConfig: CapacitorElectronConfig;
|
||||||
|
private TrayMenuTemplate: (MenuItem | MenuItemConstructorOptions)[] = [
|
||||||
|
new MenuItem({ label: 'Quit App', role: 'quit' }),
|
||||||
|
];
|
||||||
|
private AppMenuBarMenuTemplate: (MenuItem | MenuItemConstructorOptions)[] = [
|
||||||
|
{ role: process.platform === 'darwin' ? 'appMenu' : 'fileMenu' },
|
||||||
|
{ role: 'viewMenu' },
|
||||||
|
];
|
||||||
|
private mainWindowState;
|
||||||
|
private loadWebApp;
|
||||||
|
private customScheme: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
capacitorFileConfig: CapacitorElectronConfig,
|
||||||
|
trayMenuTemplate?: (MenuItemConstructorOptions | MenuItem)[],
|
||||||
|
appMenuBarMenuTemplate?: (MenuItemConstructorOptions | MenuItem)[]
|
||||||
|
) {
|
||||||
|
this.CapacitorFileConfig = capacitorFileConfig;
|
||||||
|
|
||||||
|
this.customScheme = this.CapacitorFileConfig.electron?.customUrlScheme ?? 'capacitor-electron';
|
||||||
|
|
||||||
|
if (trayMenuTemplate) {
|
||||||
|
this.TrayMenuTemplate = trayMenuTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appMenuBarMenuTemplate) {
|
||||||
|
this.AppMenuBarMenuTemplate = appMenuBarMenuTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup our web app loader, this lets us load apps like react, vue, and angular without changing their build chains.
|
||||||
|
this.loadWebApp = electronServe({
|
||||||
|
directory: join(app.getAppPath(), 'app'),
|
||||||
|
scheme: this.customScheme,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to load in the app.
|
||||||
|
private async loadMainWindow(thisRef: any) {
|
||||||
|
await thisRef.loadWebApp(thisRef.MainWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expose the mainWindow ref for use outside of the class.
|
||||||
|
getMainWindow(): BrowserWindow {
|
||||||
|
return this.MainWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCustomURLScheme(): string {
|
||||||
|
return this.customScheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(): Promise<void> {
|
||||||
|
const icon = nativeImage.createFromPath(
|
||||||
|
join(app.getAppPath(), 'assets', process.platform === 'win32' ? 'appIcon.ico' : 'appIcon.png')
|
||||||
|
);
|
||||||
|
this.mainWindowState = windowStateKeeper({
|
||||||
|
defaultWidth: 1000,
|
||||||
|
defaultHeight: 800,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup preload script path based on environment
|
||||||
|
const preloadPath = app.isPackaged
|
||||||
|
? join(process.resourcesPath, 'preload.js')
|
||||||
|
: join(__dirname, 'preload.js');
|
||||||
|
|
||||||
|
console.log('[Electron Main Process] Preload path:', preloadPath);
|
||||||
|
console.log('[Electron Main Process] Preload exists:', require('fs').existsSync(preloadPath));
|
||||||
|
|
||||||
|
this.MainWindow = new BrowserWindow({
|
||||||
|
icon,
|
||||||
|
show: false,
|
||||||
|
x: this.mainWindowState.x,
|
||||||
|
y: this.mainWindowState.y,
|
||||||
|
width: this.mainWindowState.width,
|
||||||
|
height: this.mainWindowState.height,
|
||||||
|
webPreferences: {
|
||||||
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true,
|
||||||
|
sandbox: false,
|
||||||
|
preload: preloadPath,
|
||||||
|
webSecurity: true,
|
||||||
|
allowRunningInsecureContent: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.mainWindowState.manage(this.MainWindow);
|
||||||
|
|
||||||
|
if (this.CapacitorFileConfig.backgroundColor) {
|
||||||
|
this.MainWindow.setBackgroundColor(this.CapacitorFileConfig.electron.backgroundColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we close the main window with the splashscreen enabled we need to destory the ref.
|
||||||
|
this.MainWindow.on('closed', () => {
|
||||||
|
if (this.SplashScreen?.getSplashWindow() && !this.SplashScreen.getSplashWindow().isDestroyed()) {
|
||||||
|
this.SplashScreen.getSplashWindow().close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// When the tray icon is enabled, setup the options.
|
||||||
|
if (this.CapacitorFileConfig.electron?.trayIconAndMenuEnabled) {
|
||||||
|
this.TrayIcon = new Tray(icon);
|
||||||
|
this.TrayIcon.on('double-click', () => {
|
||||||
|
if (this.MainWindow) {
|
||||||
|
if (this.MainWindow.isVisible()) {
|
||||||
|
this.MainWindow.hide();
|
||||||
|
} else {
|
||||||
|
this.MainWindow.show();
|
||||||
|
this.MainWindow.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.TrayIcon.on('click', () => {
|
||||||
|
if (this.MainWindow) {
|
||||||
|
if (this.MainWindow.isVisible()) {
|
||||||
|
this.MainWindow.hide();
|
||||||
|
} else {
|
||||||
|
this.MainWindow.show();
|
||||||
|
this.MainWindow.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.TrayIcon.setToolTip(app.getName());
|
||||||
|
this.TrayIcon.setContextMenu(Menu.buildFromTemplate(this.TrayMenuTemplate));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup the main manu bar at the top of our window.
|
||||||
|
Menu.setApplicationMenu(Menu.buildFromTemplate(this.AppMenuBarMenuTemplate));
|
||||||
|
|
||||||
|
// If the splashscreen is enabled, show it first while the main window loads then switch it out for the main window, or just load the main window from the start.
|
||||||
|
if (this.CapacitorFileConfig.electron?.splashScreenEnabled) {
|
||||||
|
this.SplashScreen = new CapacitorSplashScreen({
|
||||||
|
imageFilePath: join(
|
||||||
|
app.getAppPath(),
|
||||||
|
'assets',
|
||||||
|
this.CapacitorFileConfig.electron?.splashScreenImageName ?? 'splash.png'
|
||||||
|
),
|
||||||
|
windowWidth: 400,
|
||||||
|
windowHeight: 400,
|
||||||
|
});
|
||||||
|
this.SplashScreen.init(this.loadMainWindow, this);
|
||||||
|
} else {
|
||||||
|
this.loadMainWindow(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Security
|
||||||
|
this.MainWindow.webContents.setWindowOpenHandler((details) => {
|
||||||
|
if (!details.url.includes(this.customScheme)) {
|
||||||
|
return { action: 'deny' };
|
||||||
|
} else {
|
||||||
|
return { action: 'allow' };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.MainWindow.webContents.on('will-navigate', (event, _newURL) => {
|
||||||
|
if (!this.MainWindow.webContents.getURL().includes(this.customScheme)) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Link electron plugins into the system.
|
||||||
|
setupCapacitorElectronPlugins();
|
||||||
|
|
||||||
|
// When the web app is loaded we hide the splashscreen if needed and show the mainwindow.
|
||||||
|
this.MainWindow.webContents.on('dom-ready', () => {
|
||||||
|
if (this.CapacitorFileConfig.electron?.splashScreenEnabled) {
|
||||||
|
this.SplashScreen.getSplashWindow().hide();
|
||||||
|
}
|
||||||
|
if (!this.CapacitorFileConfig.electron?.hideMainWindowOnLaunch) {
|
||||||
|
this.MainWindow.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-register SQLite handlers after reload
|
||||||
|
if (electronIsDev) {
|
||||||
|
console.log('[Electron Main Process] Re-registering SQLite handlers after reload');
|
||||||
|
const { setupSQLiteHandlers } = require('./rt/sqlite-init');
|
||||||
|
setupSQLiteHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (electronIsDev) {
|
||||||
|
this.MainWindow.webContents.openDevTools();
|
||||||
|
}
|
||||||
|
CapElectronEventEmitter.emit('CAPELECTRON_DeeplinkListenerInitialized', '');
|
||||||
|
}, 400);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a CSP up for our application based on the custom scheme
|
||||||
|
export function setupContentSecurityPolicy(customScheme: string): void {
|
||||||
|
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
|
||||||
|
callback({
|
||||||
|
responseHeaders: {
|
||||||
|
...details.responseHeaders,
|
||||||
|
'Content-Security-Policy': [
|
||||||
|
// Base CSP for both dev and prod
|
||||||
|
`default-src ${customScheme}://*;`,
|
||||||
|
// Script sources
|
||||||
|
`script-src ${customScheme}://* 'self' 'unsafe-inline'${electronIsDev ? " 'unsafe-eval'" : ''};`,
|
||||||
|
// Style sources
|
||||||
|
`style-src ${customScheme}://* 'self' 'unsafe-inline' https://fonts.googleapis.com;`,
|
||||||
|
// Font sources
|
||||||
|
`font-src ${customScheme}://* 'self' https://fonts.gstatic.com;`,
|
||||||
|
// Image sources
|
||||||
|
`img-src ${customScheme}://* 'self' data: https:;`,
|
||||||
|
// Connect sources (for API calls)
|
||||||
|
`connect-src ${customScheme}://* 'self' https:;`,
|
||||||
|
// Worker sources
|
||||||
|
`worker-src ${customScheme}://* 'self' blob:;`,
|
||||||
|
// Frame sources
|
||||||
|
`frame-src ${customScheme}://* 'self';`,
|
||||||
|
// Media sources
|
||||||
|
`media-src ${customScheme}://* 'self' data:;`,
|
||||||
|
// Object sources
|
||||||
|
`object-src 'none';`,
|
||||||
|
// Base URI
|
||||||
|
`base-uri 'self';`,
|
||||||
|
// Form action
|
||||||
|
`form-action ${customScheme}://* 'self';`,
|
||||||
|
// Frame ancestors
|
||||||
|
`frame-ancestors 'none';`,
|
||||||
|
// Upgrade insecure requests
|
||||||
|
'upgrade-insecure-requests;',
|
||||||
|
// Block mixed content
|
||||||
|
'block-all-mixed-content;'
|
||||||
|
].join(' ')
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
18
electron/tsconfig.json
Normal file
18
electron/tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compileOnSave": true,
|
||||||
|
"include": ["./src/**/*", "./capacitor.config.ts", "./capacitor.config.js"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./build",
|
||||||
|
"importHelpers": true,
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"typeRoots": ["./node_modules/@types"],
|
||||||
|
"allowJs": true,
|
||||||
|
"rootDir": ".",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"resolveJsonModule": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
155
experiment.sh
Executable file
155
experiment.sh
Executable file
@@ -0,0 +1,155 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# experiment.sh
|
||||||
|
# Author: Matthew Raymer
|
||||||
|
# Description: Build script for TimeSafari Electron application
|
||||||
|
# This script handles the complete build process for the TimeSafari Electron app,
|
||||||
|
# including web asset compilation and Capacitor sync.
|
||||||
|
#
|
||||||
|
# Build Process:
|
||||||
|
# 1. Environment setup and dependency checks
|
||||||
|
# 2. Web asset compilation (Vite)
|
||||||
|
# 3. Capacitor sync
|
||||||
|
# 4. Electron start
|
||||||
|
#
|
||||||
|
# Dependencies:
|
||||||
|
# - Node.js and npm
|
||||||
|
# - TypeScript
|
||||||
|
# - Vite
|
||||||
|
# - @capacitor-community/electron
|
||||||
|
#
|
||||||
|
# Usage: ./experiment.sh
|
||||||
|
#
|
||||||
|
# Exit Codes:
|
||||||
|
# 1 - Required command not found
|
||||||
|
# 2 - TypeScript installation failed
|
||||||
|
# 3 - Build process failed
|
||||||
|
# 4 - Capacitor sync failed
|
||||||
|
# 5 - Electron start failed
|
||||||
|
|
||||||
|
# Exit on any error
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# ANSI color codes for better output formatting
|
||||||
|
readonly RED='\033[0;31m'
|
||||||
|
readonly GREEN='\033[0;32m'
|
||||||
|
readonly YELLOW='\033[1;33m'
|
||||||
|
readonly BLUE='\033[0;34m'
|
||||||
|
readonly NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Logging functions
|
||||||
|
log_info() {
|
||||||
|
echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')] [INFO]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_success() {
|
||||||
|
echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')] [SUCCESS]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_warn() {
|
||||||
|
echo -e "${YELLOW}[$(date '+%Y-%m-%d %H:%M:%S')] [WARN]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_error() {
|
||||||
|
echo -e "${RED}[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR]${NC} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to check if a command exists
|
||||||
|
check_command() {
|
||||||
|
if ! command -v "$1" &> /dev/null; then
|
||||||
|
log_error "$1 is required but not installed."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
log_info "Found $1: $(command -v "$1")"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to measure and log execution time
|
||||||
|
measure_time() {
|
||||||
|
local start_time=$(date +%s)
|
||||||
|
"$@"
|
||||||
|
local end_time=$(date +%s)
|
||||||
|
local duration=$((end_time - start_time))
|
||||||
|
log_success "Completed in ${duration} seconds"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Print build header
|
||||||
|
echo -e "\n${BLUE}=== TimeSafari Electron Build Process ===${NC}\n"
|
||||||
|
log_info "Starting build process at $(date)"
|
||||||
|
|
||||||
|
# Check required commands
|
||||||
|
log_info "Checking required dependencies..."
|
||||||
|
check_command node
|
||||||
|
check_command npm
|
||||||
|
check_command git
|
||||||
|
|
||||||
|
# Create application data directory
|
||||||
|
log_info "Setting up application directories..."
|
||||||
|
mkdir -p ~/.local/share/TimeSafari/timesafari
|
||||||
|
|
||||||
|
# Clean up previous builds
|
||||||
|
log_info "Cleaning previous builds..."
|
||||||
|
rm -rf dist* || log_warn "No previous builds to clean"
|
||||||
|
|
||||||
|
# Set environment variables for the build
|
||||||
|
log_info "Configuring build environment..."
|
||||||
|
export VITE_PLATFORM=electron
|
||||||
|
export VITE_PWA_ENABLED=false
|
||||||
|
export VITE_DISABLE_PWA=true
|
||||||
|
export DEBUG_MIGRATIONS=0
|
||||||
|
|
||||||
|
# Ensure TypeScript is installed
|
||||||
|
log_info "Verifying TypeScript installation..."
|
||||||
|
if [ ! -f "./node_modules/.bin/tsc" ]; then
|
||||||
|
log_info "Installing TypeScript..."
|
||||||
|
if ! npm install --save-dev typescript@~5.2.2; then
|
||||||
|
log_error "TypeScript installation failed!"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
# Verify installation
|
||||||
|
if [ ! -f "./node_modules/.bin/tsc" ]; then
|
||||||
|
log_error "TypeScript installation verification failed!"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
log_success "TypeScript installed successfully"
|
||||||
|
else
|
||||||
|
log_info "TypeScript already installed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get git hash for versioning
|
||||||
|
GIT_HASH=$(git log -1 --pretty=format:%h)
|
||||||
|
log_info "Using git hash: ${GIT_HASH}"
|
||||||
|
|
||||||
|
# Build web assets
|
||||||
|
log_info "Building web assets with Vite..."
|
||||||
|
if ! measure_time env VITE_GIT_HASH="$GIT_HASH" npx vite build --config vite.config.app.electron.mts --mode electron; then
|
||||||
|
log_error "Web asset build failed!"
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sync with Capacitor
|
||||||
|
log_info "Syncing with Capacitor..."
|
||||||
|
if ! measure_time npx cap sync electron; then
|
||||||
|
log_error "Capacitor sync failed!"
|
||||||
|
exit 4
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Restore capacitor config
|
||||||
|
log_info "Restoring capacitor config..."
|
||||||
|
if ! git checkout electron/capacitor.config.json; then
|
||||||
|
log_error "Failed to restore capacitor config!"
|
||||||
|
exit 4
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start Electron
|
||||||
|
log_info "Starting Electron..."
|
||||||
|
cd electron/
|
||||||
|
if ! measure_time npm run electron:start; then
|
||||||
|
log_error "Electron start failed!"
|
||||||
|
exit 5
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Print build summary
|
||||||
|
log_success "Build and start completed successfully!"
|
||||||
|
echo -e "\n${GREEN}=== End of Build Process ===${NC}\n"
|
||||||
|
|
||||||
|
# Exit with success
|
||||||
|
exit 0
|
||||||
948
package-lock.json
generated
948
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
47
package.json
47
package.json
@@ -11,7 +11,7 @@
|
|||||||
"build": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.mts",
|
"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": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src",
|
||||||
"lint-fix": "eslint --ext .js,.ts,.vue --ignore-path .gitignore --fix 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.cjs && node scripts/copy-wasm.cjs",
|
||||||
"test:all": "npm run test:prerequisites && npm run build && npm run test:web && npm run test:mobile",
|
"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:prerequisites": "node scripts/check-prerequisites.js",
|
||||||
"test:web": "npx playwright test -c playwright.config-local.ts --trace on",
|
"test:web": "npx playwright test -c playwright.config-local.ts --trace on",
|
||||||
@@ -22,14 +22,15 @@
|
|||||||
"check:ios-device": "xcrun xctrace list devices 2>&1 | grep -w 'Booted' || (echo 'No iOS simulator running' && exit 1)",
|
"check:ios-device": "xcrun xctrace list devices 2>&1 | grep -w 'Booted' || (echo 'No iOS simulator running' && exit 1)",
|
||||||
"clean:electron": "rimraf dist-electron",
|
"clean:electron": "rimraf dist-electron",
|
||||||
"build:pywebview": "vite build --config vite.config.pywebview.mts",
|
"build:pywebview": "vite build --config vite.config.pywebview.mts",
|
||||||
"build:electron": "npm run clean:electron && tsc -p tsconfig.electron.json && vite build --config vite.config.electron.mts && node scripts/build-electron.js",
|
|
||||||
"build:capacitor": "vite build --mode capacitor --config vite.config.capacitor.mts",
|
|
||||||
"build:web": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.web.mts",
|
"build:web": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.web.mts",
|
||||||
|
"build:web:electron": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.web.mts && VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.electron.mts --mode electron",
|
||||||
|
"build:electron": "npm run clean:electron && npm run build:web:electron && tsc -p tsconfig.electron.json && vite build --config vite.config.electron.mts && node scripts/build-electron.cjs",
|
||||||
|
"build:capacitor": "vite build --mode capacitor --config vite.config.capacitor.mts",
|
||||||
"electron:dev": "npm run build && electron .",
|
"electron:dev": "npm run build && electron .",
|
||||||
"electron:start": "electron .",
|
"electron:start": "electron .",
|
||||||
"clean:android": "adb uninstall app.timesafari.app || true",
|
"clean:android": "adb uninstall app.timesafari.app || true",
|
||||||
"build:android": "npm run clean:android && rm -rf dist && npm run build:web && npm run build:capacitor && cd android && ./gradlew clean && ./gradlew assembleDebug && cd .. && npx cap sync android && npx capacitor-assets generate --android && npx cap open android",
|
"build:android": "npm run clean:android && rm -rf dist && npm run build:web && npm run build:capacitor && cd android && ./gradlew clean && ./gradlew assembleDebug && cd .. && npx cap sync android && npx capacitor-assets generate --android && npx cap open android",
|
||||||
"electron:build-linux": "npm run build:electron && electron-builder --linux AppImage",
|
"electron:build-linux": "electron-builder --linux AppImage",
|
||||||
"electron:build-linux-deb": "npm run build:electron && electron-builder --linux deb",
|
"electron:build-linux-deb": "npm run build:electron && electron-builder --linux deb",
|
||||||
"electron:build-linux-prod": "NODE_ENV=production npm run build:electron && electron-builder --linux AppImage",
|
"electron:build-linux-prod": "NODE_ENV=production npm run build:electron && electron-builder --linux AppImage",
|
||||||
"build:electron-prod": "NODE_ENV=production npm run build:electron",
|
"build:electron-prod": "NODE_ENV=production npm run build:electron",
|
||||||
@@ -57,8 +58,8 @@
|
|||||||
"@capacitor/ios": "^6.2.0",
|
"@capacitor/ios": "^6.2.0",
|
||||||
"@capacitor/share": "^6.0.3",
|
"@capacitor/share": "^6.0.3",
|
||||||
"@capawesome/capacitor-file-picker": "^6.2.0",
|
"@capawesome/capacitor-file-picker": "^6.2.0",
|
||||||
"@dicebear/collection": "^5.4.1",
|
"@dicebear/collection": "^5.4.3",
|
||||||
"@dicebear/core": "^5.4.1",
|
"@dicebear/core": "^5.4.3",
|
||||||
"@ethersproject/hdnode": "^5.7.0",
|
"@ethersproject/hdnode": "^5.7.0",
|
||||||
"@ethersproject/wallet": "^5.8.0",
|
"@ethersproject/wallet": "^5.8.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||||
@@ -69,7 +70,7 @@
|
|||||||
"@peculiar/asn1-schema": "^2.3.8",
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
|
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
|
||||||
"@simplewebauthn/browser": "^10.0.0",
|
"@simplewebauthn/browser": "^10.0.0",
|
||||||
"@simplewebauthn/server": "^10.0.0",
|
"@simplewebauthn/server": "^10.0.1",
|
||||||
"@tweenjs/tween.js": "^21.1.1",
|
"@tweenjs/tween.js": "^21.1.1",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@veramo/core": "^5.6.0",
|
"@veramo/core": "^5.6.0",
|
||||||
@@ -86,6 +87,7 @@
|
|||||||
"absurd-sql": "^0.0.54",
|
"absurd-sql": "^0.0.54",
|
||||||
"asn1-ber": "^1.2.2",
|
"asn1-ber": "^1.2.2",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
|
"better-sqlite3-multiple-ciphers": "^11.10.0",
|
||||||
"cbor-x": "^1.5.9",
|
"cbor-x": "^1.5.9",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"dexie": "^3.2.7",
|
"dexie": "^3.2.7",
|
||||||
@@ -93,22 +95,23 @@
|
|||||||
"did-jwt": "^7.4.7",
|
"did-jwt": "^7.4.7",
|
||||||
"did-resolver": "^4.1.0",
|
"did-resolver": "^4.1.0",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"ethereum-cryptography": "^2.1.3",
|
"electron-json-storage": "^4.6.0",
|
||||||
|
"ethereum-cryptography": "^2.2.1",
|
||||||
"ethereumjs-util": "^7.1.5",
|
"ethereumjs-util": "^7.1.5",
|
||||||
"jdenticon": "^3.2.0",
|
"jdenticon": "^3.3.0",
|
||||||
"js-generate-password": "^0.1.9",
|
"js-generate-password": "^0.1.9",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jsqr": "^1.4.0",
|
"jsqr": "^1.4.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"localstorage-slim": "^2.7.0",
|
"localstorage-slim": "^2.7.0",
|
||||||
"lru-cache": "^10.2.0",
|
"lru-cache": "^10.4.3",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"merkletreejs": "^0.3.11",
|
"merkletreejs": "^0.3.11",
|
||||||
"nostr-tools": "^2.10.4",
|
"nostr-tools": "^2.13.1",
|
||||||
"notiwind": "^2.0.2",
|
"notiwind": "^2.0.2",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
"pina": "^0.20.2204228",
|
"pina": "^0.20.2204228",
|
||||||
"pinia-plugin-persistedstate": "^3.2.1",
|
"pinia-plugin-persistedstate": "^3.2.3",
|
||||||
"qr-code-generator-vue3": "^1.4.21",
|
"qr-code-generator-vue3": "^1.4.21",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"ramda": "^0.29.1",
|
"ramda": "^0.29.1",
|
||||||
@@ -124,12 +127,13 @@
|
|||||||
"vue-axios": "^3.5.2",
|
"vue-axios": "^3.5.2",
|
||||||
"vue-facing-decorator": "^3.0.4",
|
"vue-facing-decorator": "^3.0.4",
|
||||||
"vue-picture-cropper": "^0.7.0",
|
"vue-picture-cropper": "^0.7.0",
|
||||||
"vue-qrcode-reader": "^5.5.3",
|
"vue-qrcode-reader": "^5.7.2",
|
||||||
"vue-router": "^4.5.0",
|
"vue-router": "^4.5.0",
|
||||||
"web-did-resolver": "^2.0.27",
|
"web-did-resolver": "^2.0.30",
|
||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@capacitor-community/electron": "^5.0.1",
|
||||||
"@capacitor/assets": "^3.0.5",
|
"@capacitor/assets": "^3.0.5",
|
||||||
"@playwright/test": "^1.45.2",
|
"@playwright/test": "^1.45.2",
|
||||||
"@types/dom-webcodecs": "^0.1.7",
|
"@types/dom-webcodecs": "^0.1.7",
|
||||||
@@ -144,7 +148,7 @@
|
|||||||
"@types/ua-parser-js": "^0.7.39",
|
"@types/ua-parser-js": "^0.7.39",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
||||||
"@typescript-eslint/parser": "^6.21.0",
|
"@typescript-eslint/parser": "^6.21.0",
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/plugin-vue": "^5.2.4",
|
||||||
"@vue/eslint-config-typescript": "^11.0.3",
|
"@vue/eslint-config-typescript": "^11.0.3",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"browserify-fs": "^1.0.0",
|
"browserify-fs": "^1.0.0",
|
||||||
@@ -164,12 +168,13 @@
|
|||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"typescript": "~5.2.2",
|
"typescript": "~5.2.2",
|
||||||
"vite": "^5.2.0",
|
"vite": "^5.2.0",
|
||||||
"vite-plugin-pwa": "^1.0.0"
|
"vite-plugin-pwa": "^1.0.0"
|
||||||
},
|
},
|
||||||
"main": "./dist-electron/main.js",
|
"main": "./dist-electron/main.mjs",
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "app.timesafari",
|
"appId": "app.timesafari",
|
||||||
"productName": "TimeSafari",
|
"productName": "TimeSafari",
|
||||||
@@ -178,12 +183,17 @@
|
|||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist-electron/**/*",
|
"dist-electron/**/*",
|
||||||
"dist/**/*"
|
"dist/**/*",
|
||||||
|
"capacitor.config.json"
|
||||||
],
|
],
|
||||||
"extraResources": [
|
"extraResources": [
|
||||||
{
|
{
|
||||||
"from": "dist-electron/www",
|
"from": "dist-electron/www",
|
||||||
"to": "www"
|
"to": "www"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "dist-electron/resources/preload.js",
|
||||||
|
"to": "preload.js"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"linux": {
|
"linux": {
|
||||||
@@ -221,5 +231,6 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"type": "module"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
eth_keys
|
eth_keys
|
||||||
pywebview
|
pywebview
|
||||||
pyinstaller>=6.12.0
|
pyinstaller>=6.12.0
|
||||||
|
setuptools>=69.0.0 # Required for distutils for electron-builder on macOS
|
||||||
# For development
|
# For development
|
||||||
watchdog>=3.0.0 # For file watching support
|
watchdog>=3.0.0 # For file watching support
|
||||||
96
scripts/build-electron.cjs
Normal file
96
scripts/build-electron.cjs
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const fse = require("fs-extra");
|
||||||
|
const path = require("path");
|
||||||
|
const { execSync } = require('child_process');
|
||||||
|
|
||||||
|
console.log("Starting Electron build finalization...");
|
||||||
|
|
||||||
|
// Define paths
|
||||||
|
const distPath = path.join(__dirname, "..", "dist");
|
||||||
|
const electronDistPath = path.join(__dirname, "..", "dist-electron");
|
||||||
|
const wwwPath = path.join(electronDistPath, "www");
|
||||||
|
const builtIndexPath = path.join(distPath, "index.html");
|
||||||
|
const finalIndexPath = path.join(wwwPath, "index.html");
|
||||||
|
|
||||||
|
// Ensure target directory exists
|
||||||
|
if (!fs.existsSync(wwwPath)) {
|
||||||
|
fs.mkdirSync(wwwPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy assets directory
|
||||||
|
const assetsSrc = path.join(distPath, "assets");
|
||||||
|
const assetsDest = path.join(wwwPath, "assets");
|
||||||
|
if (fs.existsSync(assetsSrc)) {
|
||||||
|
fse.copySync(assetsSrc, assetsDest, { overwrite: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy favicon.ico
|
||||||
|
const faviconSrc = path.join(distPath, "favicon.ico");
|
||||||
|
if (fs.existsSync(faviconSrc)) {
|
||||||
|
fs.copyFileSync(faviconSrc, path.join(wwwPath, "favicon.ico"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy manifest.webmanifest
|
||||||
|
const manifestSrc = path.join(distPath, "manifest.webmanifest");
|
||||||
|
if (fs.existsSync(manifestSrc)) {
|
||||||
|
fs.copyFileSync(manifestSrc, path.join(wwwPath, "manifest.webmanifest"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load and modify index.html from Vite output
|
||||||
|
let indexContent = fs.readFileSync(builtIndexPath, "utf-8");
|
||||||
|
|
||||||
|
// Inject the window.process shim after the first <script> block
|
||||||
|
indexContent = indexContent.replace(
|
||||||
|
/<script[^>]*type="module"[^>]*>/,
|
||||||
|
match => `${match}\n window.process = { env: { VITE_PLATFORM: 'electron' } };`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Write the modified index.html to dist-electron/www
|
||||||
|
fs.writeFileSync(finalIndexPath, indexContent);
|
||||||
|
|
||||||
|
// Copy preload script to resources
|
||||||
|
const preloadSrc = path.join(electronDistPath, "preload.mjs");
|
||||||
|
const preloadDest = path.join(electronDistPath, "resources", "preload.js");
|
||||||
|
|
||||||
|
// Ensure resources directory exists
|
||||||
|
const resourcesDir = path.join(electronDistPath, "resources");
|
||||||
|
if (!fs.existsSync(resourcesDir)) {
|
||||||
|
fs.mkdirSync(resourcesDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(preloadSrc)) {
|
||||||
|
// Read the preload script
|
||||||
|
let preloadContent = fs.readFileSync(preloadSrc, 'utf-8');
|
||||||
|
|
||||||
|
// Convert ESM to CommonJS if needed
|
||||||
|
preloadContent = preloadContent
|
||||||
|
.replace(/import\s*{\s*([^}]+)\s*}\s*from\s*['"]electron['"];?/g, 'const { $1 } = require("electron");')
|
||||||
|
.replace(/export\s*{([^}]+)};?/g, '')
|
||||||
|
.replace(/export\s+default\s+([^;]+);?/g, 'module.exports = $1;');
|
||||||
|
|
||||||
|
// Write the modified preload script
|
||||||
|
fs.writeFileSync(preloadDest, preloadContent);
|
||||||
|
console.log("Preload script copied and converted to resources directory");
|
||||||
|
} else {
|
||||||
|
console.error("Preload script not found at:", preloadSrc);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy capacitor.config.json to dist-electron
|
||||||
|
try {
|
||||||
|
console.log("Copying capacitor.config.json to dist-electron...");
|
||||||
|
const configPath = path.join(process.cwd(), 'capacitor.config.json');
|
||||||
|
const targetPath = path.join(process.cwd(), 'dist-electron', 'capacitor.config.json');
|
||||||
|
|
||||||
|
if (!fs.existsSync(configPath)) {
|
||||||
|
throw new Error('capacitor.config.json not found in project root');
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.copyFileSync(configPath, targetPath);
|
||||||
|
console.log("Successfully copied capacitor.config.json");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to copy capacitor.config.json:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Electron index.html copied and patched for Electron context.");
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
console.log('Starting electron build process...');
|
|
||||||
|
|
||||||
// Define paths
|
|
||||||
const electronDistPath = path.join(__dirname, '..', 'dist-electron');
|
|
||||||
const wwwPath = path.join(electronDistPath, 'www');
|
|
||||||
|
|
||||||
// Create www directory if it doesn't exist
|
|
||||||
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>`;
|
|
||||||
|
|
||||||
// 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
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fix asset paths
|
|
||||||
console.log('Fixing asset paths in index.html...');
|
|
||||||
let modifiedIndexContent = fs.readFileSync(indexPath, 'utf8');
|
|
||||||
modifiedIndexContent = modifiedIndexContent
|
|
||||||
.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');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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('Copied and fixed web files in:', wwwPath);
|
|
||||||
|
|
||||||
// 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');
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Electron build process completed successfully');
|
|
||||||
@@ -459,9 +459,10 @@ export default class App extends Vue {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverSubscription = {
|
const serverSubscription =
|
||||||
...subscription,
|
typeof subscription === "object" && subscription !== null
|
||||||
};
|
? { ...subscription }
|
||||||
|
: {};
|
||||||
if (!allGoingOff) {
|
if (!allGoingOff) {
|
||||||
serverSubscription["notifyType"] = notification.title;
|
serverSubscription["notifyType"] = notification.title;
|
||||||
logger.log(
|
logger.log(
|
||||||
|
|||||||
@@ -320,10 +320,7 @@ export default class GiftedDialog extends Vue {
|
|||||||
this.fromProjectId,
|
this.fromProjectId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (!result.success) {
|
||||||
result.type === "error" ||
|
|
||||||
this.isGiveCreationError(result.response)
|
|
||||||
) {
|
|
||||||
const errorMessage = this.getGiveCreationErrorMessage(result);
|
const errorMessage = this.getGiveCreationErrorMessage(result);
|
||||||
logger.error("Error with give creation result:", result);
|
logger.error("Error with give creation result:", result);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
@@ -370,15 +367,6 @@ export default class GiftedDialog extends Vue {
|
|||||||
|
|
||||||
// Helper functions for readability
|
// Helper functions for readability
|
||||||
|
|
||||||
/**
|
|
||||||
* @param result response "data" from the server
|
|
||||||
* @returns true if the result indicates an error
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
isGiveCreationError(result: any) {
|
|
||||||
return result.status !== 201 || result.data?.error;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
|
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
|
||||||
* @returns best guess at an error message
|
* @returns best guess at an error message
|
||||||
|
|||||||
@@ -249,10 +249,7 @@ export default class OfferDialog extends Vue {
|
|||||||
this.projectId,
|
this.projectId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (!result.success) {
|
||||||
result.type === "error" ||
|
|
||||||
this.isOfferCreationError(result.response)
|
|
||||||
) {
|
|
||||||
const errorMessage = this.getOfferCreationErrorMessage(result);
|
const errorMessage = this.getOfferCreationErrorMessage(result);
|
||||||
logger.error("Error with offer creation result:", result);
|
logger.error("Error with offer creation result:", result);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
@@ -296,15 +293,6 @@ export default class OfferDialog extends Vue {
|
|||||||
|
|
||||||
// Helper functions for readability
|
// Helper functions for readability
|
||||||
|
|
||||||
/**
|
|
||||||
* @param result response "data" from the server
|
|
||||||
* @returns true if the result indicates an error
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
isOfferCreationError(result: any) {
|
|
||||||
return result.status !== 201 || result.data?.error;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
|
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
|
||||||
* @returns best guess at an error message
|
* @returns best guess at an error message
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
|||||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||||
import * as databaseUtil from "../db/databaseUtil";
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
||||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class UserNameDialog extends Vue {
|
export default class UserNameDialog extends Vue {
|
||||||
@@ -72,11 +71,9 @@ export default class UserNameDialog extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async onClickSaveChanges() {
|
async onClickSaveChanges() {
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
await databaseUtil.updateDefaultSettings({
|
||||||
await platformService.dbExec(
|
firstName: this.givenName,
|
||||||
"UPDATE settings SET firstName = ? WHERE key = ?",
|
});
|
||||||
[this.givenName, MASTER_SETTINGS_KEY],
|
|
||||||
);
|
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
firstName: this.givenName,
|
firstName: this.givenName,
|
||||||
|
|||||||
@@ -3,11 +3,19 @@
|
|||||||
* That file will eventually be deleted.
|
* That file will eventually be deleted.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
||||||
import { MASTER_SETTINGS_KEY, Settings } from "./tables/settings";
|
import { MASTER_SETTINGS_KEY, Settings } from "./tables/settings";
|
||||||
import { logger } from "@/utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
|
import { DEFAULT_ENDORSER_API_SERVER } from "../constants/app";
|
||||||
import { QueryExecResult } from "@/interfaces/database";
|
import { QueryExecResult } from "../interfaces/database";
|
||||||
|
|
||||||
|
const formatLogObject = (obj: unknown): string => {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(obj, null, 2);
|
||||||
|
} catch (error) {
|
||||||
|
return `[Object could not be stringified: ${error instanceof Error ? error.message : String(error)}]`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export async function updateDefaultSettings(
|
export async function updateDefaultSettings(
|
||||||
settingsChanges: Settings,
|
settingsChanges: Settings,
|
||||||
@@ -23,10 +31,13 @@ export async function updateDefaultSettings(
|
|||||||
"id = ?",
|
"id = ?",
|
||||||
[MASTER_SETTINGS_KEY],
|
[MASTER_SETTINGS_KEY],
|
||||||
);
|
);
|
||||||
|
console.log("[databaseUtil] updateDefaultSettings", { sql, params });
|
||||||
const result = await platformService.dbExec(sql, params);
|
const result = await platformService.dbExec(sql, params);
|
||||||
|
console.log("[databaseUtil] updateDefaultSettings result", { result });
|
||||||
return result.changes === 1;
|
return result.changes === 1;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error updating default settings:", error);
|
logger.error("Error updating default settings:", error);
|
||||||
|
console.log("[databaseUtil] updateDefaultSettings error", { error });
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
throw error; // Re-throw if it's already an Error with a message
|
throw error; // Re-throw if it's already an Error with a message
|
||||||
} else {
|
} else {
|
||||||
@@ -79,48 +90,78 @@ const DEFAULT_SETTINGS: Settings = {
|
|||||||
|
|
||||||
// retrieves default settings
|
// retrieves default settings
|
||||||
export async function retrieveSettingsForDefaultAccount(): Promise<Settings> {
|
export async function retrieveSettingsForDefaultAccount(): Promise<Settings> {
|
||||||
|
console.log('[DatabaseUtil] Retrieving default account settings');
|
||||||
const platform = PlatformServiceFactory.getInstance();
|
const platform = PlatformServiceFactory.getInstance();
|
||||||
|
|
||||||
|
console.log('[DatabaseUtil] Platform service state:', {
|
||||||
|
platformType: platform.constructor.name,
|
||||||
|
capabilities: platform.getCapabilities(),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
const result = await platform.dbQuery("SELECT * FROM settings WHERE id = ?", [
|
const result = await platform.dbQuery("SELECT * FROM settings WHERE id = ?", [
|
||||||
MASTER_SETTINGS_KEY,
|
MASTER_SETTINGS_KEY,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
|
console.log('[DatabaseUtil] No settings found, returning defaults');
|
||||||
return DEFAULT_SETTINGS;
|
return DEFAULT_SETTINGS;
|
||||||
} else {
|
|
||||||
const settings = mapColumnsToValues(
|
|
||||||
result.columns,
|
|
||||||
result.values,
|
|
||||||
)[0] as Settings;
|
|
||||||
if (settings.searchBoxes) {
|
|
||||||
// @ts-expect-error - the searchBoxes field is a string in the DB
|
|
||||||
settings.searchBoxes = JSON.parse(settings.searchBoxes);
|
|
||||||
}
|
|
||||||
return settings;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const settings = mapColumnsToValues(result.columns, result.values)[0] as Settings;
|
||||||
|
if (settings.searchBoxes) {
|
||||||
|
// @ts-expect-error - the searchBoxes field is a string in the DB
|
||||||
|
settings.searchBoxes = JSON.parse(settings.searchBoxes);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[DatabaseUtil] Retrieved settings:', {
|
||||||
|
settings: formatLogObject(settings),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
|
export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
|
||||||
|
console.log('[DatabaseUtil] Retrieving active account settings');
|
||||||
const defaultSettings = await retrieveSettingsForDefaultAccount();
|
const defaultSettings = await retrieveSettingsForDefaultAccount();
|
||||||
|
|
||||||
|
console.log('[DatabaseUtil] Default settings retrieved:', {
|
||||||
|
defaultSettings: formatLogObject(defaultSettings),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
if (!defaultSettings.activeDid) {
|
if (!defaultSettings.activeDid) {
|
||||||
|
console.log('[DatabaseUtil] No active DID, returning default settings');
|
||||||
return defaultSettings;
|
return defaultSettings;
|
||||||
} else {
|
|
||||||
const platform = PlatformServiceFactory.getInstance();
|
|
||||||
const result = await platform.dbQuery(
|
|
||||||
"SELECT * FROM settings WHERE accountDid = ?",
|
|
||||||
[defaultSettings.activeDid],
|
|
||||||
);
|
|
||||||
const overrideSettings = result
|
|
||||||
? (mapColumnsToValues(result.columns, result.values)[0] as Settings)
|
|
||||||
: {};
|
|
||||||
const overrideSettingsFiltered = Object.fromEntries(
|
|
||||||
Object.entries(overrideSettings).filter(([_, v]) => v !== null),
|
|
||||||
);
|
|
||||||
const settings = { ...defaultSettings, ...overrideSettingsFiltered };
|
|
||||||
if (settings.searchBoxes) {
|
|
||||||
// @ts-expect-error - the searchBoxes field is a string in the DB
|
|
||||||
settings.searchBoxes = JSON.parse(settings.searchBoxes);
|
|
||||||
}
|
|
||||||
return settings;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const platform = PlatformServiceFactory.getInstance();
|
||||||
|
const result = await platform.dbQuery(
|
||||||
|
"SELECT * FROM settings WHERE accountDid = ?",
|
||||||
|
[defaultSettings.activeDid],
|
||||||
|
);
|
||||||
|
|
||||||
|
const overrideSettings = result
|
||||||
|
? (mapColumnsToValues(result.columns, result.values)[0] as Settings)
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const overrideSettingsFiltered = Object.fromEntries(
|
||||||
|
Object.entries(overrideSettings).filter(([_, v]) => v !== null),
|
||||||
|
);
|
||||||
|
|
||||||
|
const settings = { ...defaultSettings, ...overrideSettingsFiltered };
|
||||||
|
if (settings.searchBoxes) {
|
||||||
|
// @ts-expect-error - the searchBoxes field is a string in the DB
|
||||||
|
settings.searchBoxes = JSON.parse(settings.searchBoxes);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[DatabaseUtil] Final active account settings:', {
|
||||||
|
settings: formatLogObject(settings),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastCleanupDate: string | null = null;
|
let lastCleanupDate: string | null = null;
|
||||||
@@ -131,26 +172,26 @@ let lastCleanupDate: string | null = null;
|
|||||||
* @author Matthew Raymer
|
* @author Matthew Raymer
|
||||||
*/
|
*/
|
||||||
export async function logToDb(message: string): Promise<void> {
|
export async function logToDb(message: string): Promise<void> {
|
||||||
const platform = PlatformServiceFactory.getInstance();
|
//const platform = PlatformServiceFactory.getInstance();
|
||||||
const todayKey = new Date().toDateString();
|
const todayKey = new Date().toDateString();
|
||||||
const nowKey = new Date().toISOString();
|
//const nowKey = new Date().toISOString();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Try to insert first, if it fails due to UNIQUE constraint, update instead
|
// Try to insert first, if it fails due to UNIQUE constraint, update instead
|
||||||
await platform.dbExec("INSERT INTO logs (date, message) VALUES (?, ?)", [
|
// await platform.dbExec("INSERT INTO logs (date, message) VALUES (?, ?)", [
|
||||||
nowKey,
|
// nowKey,
|
||||||
message,
|
// message,
|
||||||
]);
|
// ]);
|
||||||
|
|
||||||
// Clean up old logs (keep only last 7 days) - do this less frequently
|
// Clean up old logs (keep only last 7 days) - do this less frequently
|
||||||
// Only clean up if the date is different from the last cleanup
|
// Only clean up if the date is different from the last cleanup
|
||||||
if (!lastCleanupDate || lastCleanupDate !== todayKey) {
|
if (!lastCleanupDate || lastCleanupDate !== todayKey) {
|
||||||
const sevenDaysAgo = new Date(
|
// const sevenDaysAgo = new Date(
|
||||||
new Date().getTime() - 7 * 24 * 60 * 60 * 1000,
|
// new Date().getTime() - 7 * 24 * 60 * 60 * 1000,
|
||||||
);
|
// );
|
||||||
await platform.dbExec("DELETE FROM logs WHERE date < ?", [
|
// await platform.dbExec("DELETE FROM logs WHERE date < ?", [
|
||||||
sevenDaysAgo.toDateString(),
|
// sevenDaysAgo.toDateString(),
|
||||||
]);
|
// ]);
|
||||||
lastCleanupDate = todayKey;
|
lastCleanupDate = todayKey;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,174 +0,0 @@
|
|||||||
const { app, BrowserWindow } = require("electron");
|
|
||||||
const path = require("path");
|
|
||||||
const fs = require("fs");
|
|
||||||
const logger = require("../utils/logger");
|
|
||||||
|
|
||||||
// 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
|
|
||||||
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;" +
|
|
||||||
"img-src 'self' data: https: blob:;" +
|
|
||||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval';" +
|
|
||||||
"style-src 'self' 'unsafe-inline';" +
|
|
||||||
"font-src 'self' data:;",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
});
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
import { app, BrowserWindow } from "electron";
|
|
||||||
import path from "path";
|
|
||||||
import fs from "fs";
|
|
||||||
|
|
||||||
// Simple logger implementation
|
|
||||||
const logger = {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
log: (...args: unknown[]) => console.log(...args),
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
error: (...args: unknown[]) => console.error(...args),
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
info: (...args: unknown[]) => console.info(...args),
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
warn: (...args: unknown[]) => console.warn(...args),
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
debug: (...args: unknown[]) => console.debug(...args),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if running in dev mode
|
|
||||||
const isDev = process.argv.includes("--inspect");
|
|
||||||
|
|
||||||
function createWindow(): void {
|
|
||||||
// Add before createWindow function
|
|
||||||
const preloadPath = path.join(__dirname, "preload.js");
|
|
||||||
logger.log("Checking preload path:", preloadPath);
|
|
||||||
logger.log("Preload exists:", fs.existsSync(preloadPath));
|
|
||||||
|
|
||||||
// Log environment and paths
|
|
||||||
logger.log("process.cwd():", process.cwd());
|
|
||||||
logger.log("__dirname:", __dirname);
|
|
||||||
logger.log("app.getAppPath():", app.getAppPath());
|
|
||||||
logger.log("app.isPackaged:", app.isPackaged);
|
|
||||||
|
|
||||||
// List files in __dirname and __dirname/www
|
|
||||||
try {
|
|
||||||
logger.log("Files in __dirname:", fs.readdirSync(__dirname));
|
|
||||||
const wwwDir = path.join(__dirname, "www");
|
|
||||||
if (fs.existsSync(wwwDir)) {
|
|
||||||
logger.log("Files in www:", fs.readdirSync(wwwDir));
|
|
||||||
} else {
|
|
||||||
logger.log("www directory does not exist in __dirname");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logger.error("Error reading directories:", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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());
|
|
||||||
}
|
|
||||||
|
|
||||||
let indexPath = path.resolve(__dirname, "dist-electron", "www", "index.html");
|
|
||||||
if (!fs.existsSync(indexPath)) {
|
|
||||||
// Fallback for dev mode
|
|
||||||
indexPath = path.resolve(
|
|
||||||
process.cwd(),
|
|
||||||
"dist-electron",
|
|
||||||
"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
|
|
||||||
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;" +
|
|
||||||
"img-src 'self' data: https: blob:;" +
|
|
||||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval';" +
|
|
||||||
"style-src 'self' 'unsafe-inline';" +
|
|
||||||
"font-src 'self' data:;",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
});
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
const { contextBridge, ipcRenderer } = require("electron");
|
|
||||||
|
|
||||||
const logger = {
|
|
||||||
log: (message, ...args) => {
|
|
||||||
// Always log in development, log with context in production
|
|
||||||
if (process.env.NODE_ENV !== "production") {
|
|
||||||
/* eslint-disable no-console */
|
|
||||||
console.log(`[Preload] ${message}`, ...args);
|
|
||||||
/* eslint-enable no-console */
|
|
||||||
}
|
|
||||||
},
|
|
||||||
warn: (message, ...args) => {
|
|
||||||
// Always log warnings
|
|
||||||
/* eslint-disable no-console */
|
|
||||||
console.warn(`[Preload] ${message}`, ...args);
|
|
||||||
/* eslint-enable no-console */
|
|
||||||
},
|
|
||||||
error: (message, ...args) => {
|
|
||||||
// Always log errors
|
|
||||||
/* eslint-disable no-console */
|
|
||||||
console.error(`[Preload] ${message}`, ...args);
|
|
||||||
/* eslint-enable no-console */
|
|
||||||
},
|
|
||||||
info: (message, ...args) => {
|
|
||||||
// Always log info in development, log with context in production
|
|
||||||
if (process.env.NODE_ENV !== "production") {
|
|
||||||
/* eslint-disable no-console */
|
|
||||||
console.info(`[Preload] ${message}`, ...args);
|
|
||||||
/* eslint-enable no-console */
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Use a more direct path resolution approach
|
|
||||||
const getPath = (pathType) => {
|
|
||||||
switch (pathType) {
|
|
||||||
case "userData":
|
|
||||||
return (
|
|
||||||
process.env.APPDATA ||
|
|
||||||
(process.platform === "darwin"
|
|
||||||
? `${process.env.HOME}/Library/Application Support`
|
|
||||||
: `${process.env.HOME}/.local/share`)
|
|
||||||
);
|
|
||||||
case "home":
|
|
||||||
return process.env.HOME;
|
|
||||||
case "appPath":
|
|
||||||
return process.resourcesPath;
|
|
||||||
default:
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.info("Preload script starting...");
|
|
||||||
|
|
||||||
// Force electron platform in the renderer process
|
|
||||||
window.process = { env: { VITE_PLATFORM: "electron" } };
|
|
||||||
|
|
||||||
try {
|
|
||||||
contextBridge.exposeInMainWorld("electronAPI", {
|
|
||||||
// Path utilities
|
|
||||||
getPath,
|
|
||||||
|
|
||||||
// IPC functions
|
|
||||||
send: (channel, data) => {
|
|
||||||
const validChannels = ["toMain"];
|
|
||||||
if (validChannels.includes(channel)) {
|
|
||||||
ipcRenderer.send(channel, data);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
receive: (channel, func) => {
|
|
||||||
const validChannels = ["fromMain"];
|
|
||||||
if (validChannels.includes(channel)) {
|
|
||||||
ipcRenderer.on(channel, (event, ...args) => func(...args));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Environment info
|
|
||||||
env: {
|
|
||||||
isElectron: true,
|
|
||||||
isDev: process.env.NODE_ENV === "development",
|
|
||||||
platform: "electron", // Explicitly set platform
|
|
||||||
},
|
|
||||||
// Path utilities
|
|
||||||
getBasePath: () => {
|
|
||||||
return process.env.NODE_ENV === "development" ? "/" : "./";
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.info("Preload script completed successfully");
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error in preload script:", error);
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,24 @@
|
|||||||
import { GenericVerifiableCredential } from "./common";
|
/**
|
||||||
|
* Types of Claims
|
||||||
|
*
|
||||||
|
* Note that these are for the claims that get signed.
|
||||||
|
* Records that are the latest edited entities are in the records.ts file.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
export interface AgreeVerifiableCredential {
|
import { ClaimObject } from "./common";
|
||||||
"@context": string;
|
|
||||||
|
export interface AgreeActionClaim extends ClaimObject {
|
||||||
|
"@context": "https://schema.org";
|
||||||
"@type": string;
|
"@type": string;
|
||||||
object: Record<string, unknown>;
|
object: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note that previous VCs may have additional fields.
|
// Note that previous VCs may have additional fields.
|
||||||
// https://endorser.ch/doc/html/transactions.html#id4
|
// https://endorser.ch/doc/html/transactions.html#id4
|
||||||
export interface GiveVerifiableCredential extends GenericVerifiableCredential {
|
export interface GiveActionClaim extends ClaimObject {
|
||||||
|
// context is optional because it might be embedded in another claim, eg. an AgreeAction
|
||||||
|
"@context"?: "https://schema.org";
|
||||||
"@type": "GiveAction";
|
"@type": "GiveAction";
|
||||||
agent?: { identifier: string };
|
agent?: { identifier: string };
|
||||||
description?: string;
|
description?: string;
|
||||||
@@ -16,43 +26,21 @@ export interface GiveVerifiableCredential extends GenericVerifiableCredential {
|
|||||||
identifier?: string;
|
identifier?: string;
|
||||||
image?: string;
|
image?: string;
|
||||||
object?: { amountOfThisGood: number; unitCode: string };
|
object?: { amountOfThisGood: number; unitCode: string };
|
||||||
provider?: GenericVerifiableCredential;
|
provider?: ClaimObject;
|
||||||
recipient?: { identifier: string };
|
recipient?: { identifier: string };
|
||||||
type: string[];
|
}
|
||||||
issuer: string;
|
|
||||||
issuanceDate: string;
|
export interface JoinActionClaim extends ClaimObject {
|
||||||
credentialSubject: {
|
agent?: { identifier: string };
|
||||||
id: string;
|
event?: { organizer?: { name: string }; name?: string; startTime?: string };
|
||||||
type: "GiveAction";
|
|
||||||
offeredBy?: {
|
|
||||||
type: "Person";
|
|
||||||
identifier: string;
|
|
||||||
};
|
|
||||||
offeredTo?: {
|
|
||||||
type: "Person";
|
|
||||||
identifier: string;
|
|
||||||
};
|
|
||||||
offeredToProject?: {
|
|
||||||
type: "Project";
|
|
||||||
identifier: string;
|
|
||||||
};
|
|
||||||
offeredToProjectVisibleToDids?: string[];
|
|
||||||
offeredToVisibleToDids?: string[];
|
|
||||||
offeredByVisibleToDids?: string[];
|
|
||||||
amount: {
|
|
||||||
type: "QuantitativeValue";
|
|
||||||
value: number;
|
|
||||||
unitCode: string;
|
|
||||||
};
|
|
||||||
startTime?: string;
|
|
||||||
endTime?: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note that previous VCs may have additional fields.
|
// Note that previous VCs may have additional fields.
|
||||||
// https://endorser.ch/doc/html/transactions.html#id8
|
// https://endorser.ch/doc/html/transactions.html#id8
|
||||||
export interface OfferVerifiableCredential extends GenericVerifiableCredential {
|
export interface OfferClaim extends ClaimObject {
|
||||||
|
"@context": "https://schema.org";
|
||||||
"@type": "Offer";
|
"@type": "Offer";
|
||||||
|
agent?: { identifier: string };
|
||||||
description?: string;
|
description?: string;
|
||||||
fulfills?: { "@type": string; identifier?: string; lastClaimId?: string }[];
|
fulfills?: { "@type": string; identifier?: string; lastClaimId?: string }[];
|
||||||
identifier?: string;
|
identifier?: string;
|
||||||
@@ -67,43 +55,18 @@ export interface OfferVerifiableCredential extends GenericVerifiableCredential {
|
|||||||
name?: string;
|
name?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
provider?: GenericVerifiableCredential;
|
offeredBy?: {
|
||||||
|
type?: "Person";
|
||||||
|
identifier: string;
|
||||||
|
};
|
||||||
|
provider?: ClaimObject;
|
||||||
recipient?: { identifier: string };
|
recipient?: { identifier: string };
|
||||||
validThrough?: string;
|
validThrough?: string;
|
||||||
type: string[];
|
|
||||||
issuer: string;
|
|
||||||
issuanceDate: string;
|
|
||||||
credentialSubject: {
|
|
||||||
id: string;
|
|
||||||
type: "Offer";
|
|
||||||
offeredBy?: {
|
|
||||||
type: "Person";
|
|
||||||
identifier: string;
|
|
||||||
};
|
|
||||||
offeredTo?: {
|
|
||||||
type: "Person";
|
|
||||||
identifier: string;
|
|
||||||
};
|
|
||||||
offeredToProject?: {
|
|
||||||
type: "Project";
|
|
||||||
identifier: string;
|
|
||||||
};
|
|
||||||
offeredToProjectVisibleToDids?: string[];
|
|
||||||
offeredToVisibleToDids?: string[];
|
|
||||||
offeredByVisibleToDids?: string[];
|
|
||||||
amount: {
|
|
||||||
type: "QuantitativeValue";
|
|
||||||
value: number;
|
|
||||||
unitCode: string;
|
|
||||||
};
|
|
||||||
startTime?: string;
|
|
||||||
endTime?: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note that previous VCs may have additional fields.
|
// Note that previous VCs may have additional fields.
|
||||||
// https://endorser.ch/doc/html/transactions.html#id7
|
// https://endorser.ch/doc/html/transactions.html#id7
|
||||||
export interface PlanVerifiableCredential extends GenericVerifiableCredential {
|
export interface PlanActionClaim extends ClaimObject {
|
||||||
"@context": "https://schema.org";
|
"@context": "https://schema.org";
|
||||||
"@type": "PlanAction";
|
"@type": "PlanAction";
|
||||||
name: string;
|
name: string;
|
||||||
@@ -117,11 +80,18 @@ export interface PlanVerifiableCredential extends GenericVerifiableCredential {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AKA Registration & RegisterAction
|
// AKA Registration & RegisterAction
|
||||||
export interface RegisterVerifiableCredential {
|
export interface RegisterActionClaim extends ClaimObject {
|
||||||
"@context": string;
|
"@context": "https://schema.org";
|
||||||
"@type": "RegisterAction";
|
"@type": "RegisterAction";
|
||||||
agent: { identifier: string };
|
agent: { identifier: string };
|
||||||
identifier?: string;
|
identifier?: string;
|
||||||
object: string;
|
object?: string;
|
||||||
participant?: { identifier: string };
|
participant?: { identifier: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TenureClaim extends ClaimObject {
|
||||||
|
"@context": "https://endorser.ch";
|
||||||
|
"@type": "Tenure";
|
||||||
|
party?: { identifier: string };
|
||||||
|
spatialUnit?: { geo?: { polygon?: string } };
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// similar to VerifiableCredentialSubject... maybe rename this
|
// similar to VerifiableCredentialSubject... maybe rename this
|
||||||
export interface GenericVerifiableCredential {
|
export interface GenericVerifiableCredential {
|
||||||
"@context": string | string[];
|
"@context"?: string;
|
||||||
"@type": string;
|
"@type": string;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
@@ -37,23 +37,26 @@ export interface ErrorResult extends ResultWithType {
|
|||||||
|
|
||||||
export interface KeyMeta {
|
export interface KeyMeta {
|
||||||
did: string;
|
did: string;
|
||||||
name?: string;
|
|
||||||
publicKeyHex: string;
|
publicKeyHex: string;
|
||||||
mnemonic: string;
|
derivationPath?: string;
|
||||||
derivationPath: string;
|
|
||||||
registered?: boolean;
|
|
||||||
profileImageUrl?: string;
|
|
||||||
identity?: string; // Stringified IIdentifier object from Veramo
|
|
||||||
passkeyCredIdHex?: string; // The Webauthn credential ID in hex, if this is from a passkey
|
passkeyCredIdHex?: string; // The Webauthn credential ID in hex, if this is from a passkey
|
||||||
[key: string]: unknown;
|
}
|
||||||
|
|
||||||
|
export interface KeyMetaMaybeWithPrivate extends KeyMeta {
|
||||||
|
mnemonic?: string; // 12 or 24 words encoding the seed
|
||||||
|
identity?: string; // Stringified IIdentifier object from Veramo
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KeyMetaWithPrivate extends KeyMeta {
|
||||||
|
mnemonic: string; // 12 or 24 words encoding the seed
|
||||||
|
identity: string; // Stringified IIdentifier object from Veramo
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QuantitativeValue extends GenericVerifiableCredential {
|
export interface QuantitativeValue extends GenericVerifiableCredential {
|
||||||
"@type": "QuantitativeValue";
|
"@type": "QuantitativeValue";
|
||||||
"@context": string | string[];
|
"@context"?: string;
|
||||||
amountOfThisGood: number;
|
amountOfThisGood: number;
|
||||||
unitCode: string;
|
unitCode: string;
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AxiosErrorResponse {
|
export interface AxiosErrorResponse {
|
||||||
@@ -87,94 +90,21 @@ export interface CreateAndSubmitClaimResult {
|
|||||||
handleId?: string;
|
handleId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlanSummaryRecord {
|
|
||||||
handleId: string;
|
|
||||||
issuer: string;
|
|
||||||
claim: GenericVerifiableCredential;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Agent {
|
export interface Agent {
|
||||||
identifier?: string;
|
identifier?: string;
|
||||||
did?: string;
|
did?: string;
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClaimObject {
|
export interface ClaimObject {
|
||||||
"@type": string;
|
"@type": string;
|
||||||
"@context"?: string | string[];
|
"@context"?: string;
|
||||||
fulfills?: Array<{
|
|
||||||
"@type": string;
|
|
||||||
identifier?: string;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}>;
|
|
||||||
object?: GenericVerifiableCredential;
|
|
||||||
agent?: Agent;
|
|
||||||
participant?: {
|
|
||||||
identifier?: string;
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
identifier?: string;
|
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VerifiableCredentialClaim {
|
export interface VerifiableCredentialClaim {
|
||||||
"@context": string | string[];
|
"@context"?: string;
|
||||||
"@type": string;
|
"@type": string;
|
||||||
type: string[];
|
type: string[];
|
||||||
credentialSubject: ClaimObject;
|
credentialSubject: ClaimObject;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GiveVerifiableCredential extends GenericVerifiableCredential {
|
|
||||||
"@type": "GiveAction";
|
|
||||||
"@context": string | string[];
|
|
||||||
object?: GenericVerifiableCredential;
|
|
||||||
agent?: Agent;
|
|
||||||
participant?: {
|
|
||||||
identifier?: string;
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
fulfills?: Array<{
|
|
||||||
"@type": string;
|
|
||||||
identifier?: string;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}>;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface OfferVerifiableCredential extends GenericVerifiableCredential {
|
|
||||||
"@type": "OfferAction";
|
|
||||||
"@context": string | string[];
|
|
||||||
object?: GenericVerifiableCredential;
|
|
||||||
agent?: Agent;
|
|
||||||
participant?: {
|
|
||||||
identifier?: string;
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
itemOffered?: {
|
|
||||||
description?: string;
|
|
||||||
isPartOf?: {
|
|
||||||
"@type": string;
|
|
||||||
identifier: string;
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
[key: string]: unknown;
|
|
||||||
};
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RegisterVerifiableCredential
|
|
||||||
extends GenericVerifiableCredential {
|
|
||||||
"@type": "RegisterAction";
|
|
||||||
"@context": string | string[];
|
|
||||||
agent: {
|
|
||||||
identifier: string;
|
|
||||||
};
|
|
||||||
object: string;
|
|
||||||
participant?: {
|
|
||||||
identifier: string;
|
|
||||||
};
|
|
||||||
identifier?: string;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ export type {
|
|||||||
|
|
||||||
export type {
|
export type {
|
||||||
// From claims.ts
|
// From claims.ts
|
||||||
GiveVerifiableCredential,
|
GiveActionClaim,
|
||||||
OfferVerifiableCredential,
|
OfferClaim,
|
||||||
RegisterVerifiableCredential,
|
RegisterActionClaim,
|
||||||
} from "./claims";
|
} from "./claims";
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
@@ -26,6 +26,7 @@ export type {
|
|||||||
export type {
|
export type {
|
||||||
// From records.ts
|
// From records.ts
|
||||||
PlanSummaryRecord,
|
PlanSummaryRecord,
|
||||||
|
GiveSummaryRecord,
|
||||||
} from "./records";
|
} from "./records";
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { GiveVerifiableCredential, OfferVerifiableCredential } from "./claims";
|
import { GiveActionClaim, OfferClaim } from "./claims";
|
||||||
|
|
||||||
// a summary record; the VC is found the fullClaim field
|
// a summary record; the VC is found the fullClaim field
|
||||||
export interface GiveSummaryRecord {
|
export interface GiveSummaryRecord {
|
||||||
[x: string]: PropertyKey | undefined | GiveVerifiableCredential;
|
[x: string]: PropertyKey | undefined | GiveActionClaim;
|
||||||
type?: string;
|
type?: string;
|
||||||
agentDid: string;
|
agentDid: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
amountConfirmed: number;
|
amountConfirmed: number;
|
||||||
description: string;
|
description: string;
|
||||||
fullClaim: GiveVerifiableCredential;
|
fullClaim: GiveActionClaim;
|
||||||
fulfillsHandleId: string;
|
fulfillsHandleId: string;
|
||||||
fulfillsPlanHandleId?: string;
|
fulfillsPlanHandleId?: string;
|
||||||
fulfillsType?: string;
|
fulfillsType?: string;
|
||||||
@@ -26,7 +26,7 @@ export interface OfferSummaryRecord {
|
|||||||
amount: number;
|
amount: number;
|
||||||
amountGiven: number;
|
amountGiven: number;
|
||||||
amountGivenConfirmed: number;
|
amountGivenConfirmed: number;
|
||||||
fullClaim: OfferVerifiableCredential;
|
fullClaim: OfferClaim;
|
||||||
fulfillsPlanHandleId: string;
|
fulfillsPlanHandleId: string;
|
||||||
handleId: string;
|
handleId: string;
|
||||||
issuerDid: string;
|
issuerDid: string;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { didEthLocalResolver } from "./did-eth-local-resolver";
|
|||||||
import { PEER_DID_PREFIX, verifyPeerSignature } from "./didPeer";
|
import { PEER_DID_PREFIX, verifyPeerSignature } from "./didPeer";
|
||||||
import { base64urlDecodeString, createDidPeerJwt } from "./passkeyDidPeer";
|
import { base64urlDecodeString, createDidPeerJwt } from "./passkeyDidPeer";
|
||||||
import { urlBase64ToUint8Array } from "./util";
|
import { urlBase64ToUint8Array } from "./util";
|
||||||
import { KeyMeta } from "../../../interfaces/common";
|
import { KeyMeta, KeyMetaWithPrivate } from "../../../interfaces/common";
|
||||||
|
|
||||||
export const ETHR_DID_PREFIX = "did:ethr:";
|
export const ETHR_DID_PREFIX = "did:ethr:";
|
||||||
export const JWT_VERIFY_FAILED_CODE = "JWT_VERIFY_FAILED";
|
export const JWT_VERIFY_FAILED_CODE = "JWT_VERIFY_FAILED";
|
||||||
@@ -34,7 +34,7 @@ export function isFromPasskey(keyMeta?: KeyMeta): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function createEndorserJwtForKey(
|
export async function createEndorserJwtForKey(
|
||||||
account: KeyMeta,
|
account: KeyMetaWithPrivate,
|
||||||
payload: object,
|
payload: object,
|
||||||
expiresIn?: number,
|
expiresIn?: number,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -38,7 +38,14 @@ import {
|
|||||||
getPasskeyExpirationSeconds,
|
getPasskeyExpirationSeconds,
|
||||||
} from "../libs/util";
|
} from "../libs/util";
|
||||||
import { createEndorserJwtForKey } from "../libs/crypto/vc";
|
import { createEndorserJwtForKey } from "../libs/crypto/vc";
|
||||||
import { KeyMeta } from "../interfaces/common";
|
import {
|
||||||
|
GiveActionClaim,
|
||||||
|
JoinActionClaim,
|
||||||
|
OfferClaim,
|
||||||
|
PlanActionClaim,
|
||||||
|
RegisterActionClaim,
|
||||||
|
TenureClaim,
|
||||||
|
} from "../interfaces/claims";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
GenericCredWrapper,
|
GenericCredWrapper,
|
||||||
@@ -46,15 +53,13 @@ import {
|
|||||||
AxiosErrorResponse,
|
AxiosErrorResponse,
|
||||||
UserInfo,
|
UserInfo,
|
||||||
CreateAndSubmitClaimResult,
|
CreateAndSubmitClaimResult,
|
||||||
PlanSummaryRecord,
|
|
||||||
GiveVerifiableCredential,
|
|
||||||
OfferVerifiableCredential,
|
|
||||||
RegisterVerifiableCredential,
|
|
||||||
ClaimObject,
|
ClaimObject,
|
||||||
VerifiableCredentialClaim,
|
VerifiableCredentialClaim,
|
||||||
Agent,
|
|
||||||
QuantitativeValue,
|
QuantitativeValue,
|
||||||
|
KeyMetaWithPrivate,
|
||||||
|
KeyMetaMaybeWithPrivate,
|
||||||
} from "../interfaces/common";
|
} from "../interfaces/common";
|
||||||
|
import { PlanSummaryRecord } from "../interfaces/records";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||||
|
|
||||||
@@ -650,7 +655,7 @@ export async function getNewOffersToUserProjects(
|
|||||||
* @param lastClaimId supplied when editing a previous claim
|
* @param lastClaimId supplied when editing a previous claim
|
||||||
*/
|
*/
|
||||||
export function hydrateGive(
|
export function hydrateGive(
|
||||||
vcClaimOrig?: GiveVerifiableCredential,
|
vcClaimOrig?: GiveActionClaim,
|
||||||
fromDid?: string,
|
fromDid?: string,
|
||||||
toDid?: string,
|
toDid?: string,
|
||||||
description?: string,
|
description?: string,
|
||||||
@@ -662,15 +667,12 @@ export function hydrateGive(
|
|||||||
imageUrl?: string,
|
imageUrl?: string,
|
||||||
providerPlanHandleId?: string,
|
providerPlanHandleId?: string,
|
||||||
lastClaimId?: string,
|
lastClaimId?: string,
|
||||||
): GiveVerifiableCredential {
|
): GiveActionClaim {
|
||||||
const vcClaim: GiveVerifiableCredential = vcClaimOrig
|
const vcClaim: GiveActionClaim = vcClaimOrig
|
||||||
? R.clone(vcClaimOrig)
|
? R.clone(vcClaimOrig)
|
||||||
: {
|
: {
|
||||||
"@context": SCHEMA_ORG_CONTEXT,
|
"@context": SCHEMA_ORG_CONTEXT,
|
||||||
"@type": "GiveAction",
|
"@type": "GiveAction",
|
||||||
object: undefined,
|
|
||||||
agent: undefined,
|
|
||||||
fulfills: [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (lastClaimId) {
|
if (lastClaimId) {
|
||||||
@@ -688,7 +690,6 @@ export function hydrateGive(
|
|||||||
|
|
||||||
if (amount && !isNaN(amount)) {
|
if (amount && !isNaN(amount)) {
|
||||||
const quantitativeValue: QuantitativeValue = {
|
const quantitativeValue: QuantitativeValue = {
|
||||||
"@context": SCHEMA_ORG_CONTEXT,
|
|
||||||
"@type": "QuantitativeValue",
|
"@type": "QuantitativeValue",
|
||||||
amountOfThisGood: amount,
|
amountOfThisGood: amount,
|
||||||
unitCode: unitCode || "HUR",
|
unitCode: unitCode || "HUR",
|
||||||
@@ -698,7 +699,7 @@ export function hydrateGive(
|
|||||||
|
|
||||||
// Initialize fulfills array if not present
|
// Initialize fulfills array if not present
|
||||||
if (!Array.isArray(vcClaim.fulfills)) {
|
if (!Array.isArray(vcClaim.fulfills)) {
|
||||||
vcClaim.fulfills = [];
|
vcClaim.fulfills = vcClaim.fulfills ? [vcClaim.fulfills] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter and add fulfills elements
|
// Filter and add fulfills elements
|
||||||
@@ -801,7 +802,7 @@ export async function createAndSubmitGive(
|
|||||||
export async function editAndSubmitGive(
|
export async function editAndSubmitGive(
|
||||||
axios: Axios,
|
axios: Axios,
|
||||||
apiServer: string,
|
apiServer: string,
|
||||||
fullClaim: GenericCredWrapper<GiveVerifiableCredential>,
|
fullClaim: GenericCredWrapper<GiveActionClaim>,
|
||||||
issuerDid: string,
|
issuerDid: string,
|
||||||
fromDid?: string,
|
fromDid?: string,
|
||||||
toDid?: string,
|
toDid?: string,
|
||||||
@@ -842,7 +843,7 @@ export async function editAndSubmitGive(
|
|||||||
* @param lastClaimId supplied when editing a previous claim
|
* @param lastClaimId supplied when editing a previous claim
|
||||||
*/
|
*/
|
||||||
export function hydrateOffer(
|
export function hydrateOffer(
|
||||||
vcClaimOrig?: OfferVerifiableCredential,
|
vcClaimOrig?: OfferClaim,
|
||||||
fromDid?: string,
|
fromDid?: string,
|
||||||
toDid?: string,
|
toDid?: string,
|
||||||
itemDescription?: string,
|
itemDescription?: string,
|
||||||
@@ -852,24 +853,22 @@ export function hydrateOffer(
|
|||||||
fulfillsProjectHandleId?: string,
|
fulfillsProjectHandleId?: string,
|
||||||
validThrough?: string,
|
validThrough?: string,
|
||||||
lastClaimId?: string,
|
lastClaimId?: string,
|
||||||
): OfferVerifiableCredential {
|
): OfferClaim {
|
||||||
const vcClaim: OfferVerifiableCredential = vcClaimOrig
|
const vcClaim: OfferClaim = vcClaimOrig
|
||||||
? R.clone(vcClaimOrig)
|
? R.clone(vcClaimOrig)
|
||||||
: {
|
: {
|
||||||
"@context": SCHEMA_ORG_CONTEXT,
|
"@context": SCHEMA_ORG_CONTEXT,
|
||||||
"@type": "OfferAction",
|
"@type": "Offer",
|
||||||
object: undefined,
|
|
||||||
agent: undefined,
|
|
||||||
itemOffered: {},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (lastClaimId) {
|
if (lastClaimId) {
|
||||||
|
// this is an edit
|
||||||
vcClaim.lastClaimId = lastClaimId;
|
vcClaim.lastClaimId = lastClaimId;
|
||||||
delete vcClaim.identifier;
|
delete vcClaim.identifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fromDid) {
|
if (fromDid) {
|
||||||
vcClaim.agent = { identifier: fromDid };
|
vcClaim.offeredBy = { identifier: fromDid };
|
||||||
}
|
}
|
||||||
if (toDid) {
|
if (toDid) {
|
||||||
vcClaim.recipient = { identifier: toDid };
|
vcClaim.recipient = { identifier: toDid };
|
||||||
@@ -877,13 +876,10 @@ export function hydrateOffer(
|
|||||||
vcClaim.description = conditionDescription || undefined;
|
vcClaim.description = conditionDescription || undefined;
|
||||||
|
|
||||||
if (amount && !isNaN(amount)) {
|
if (amount && !isNaN(amount)) {
|
||||||
const quantitativeValue: QuantitativeValue = {
|
vcClaim.includesObject = {
|
||||||
"@context": SCHEMA_ORG_CONTEXT,
|
|
||||||
"@type": "QuantitativeValue",
|
|
||||||
amountOfThisGood: amount,
|
amountOfThisGood: amount,
|
||||||
unitCode: unitCode || "HUR",
|
unitCode: unitCode || "HUR",
|
||||||
};
|
};
|
||||||
vcClaim.object = quantitativeValue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (itemDescription || fulfillsProjectHandleId) {
|
if (itemDescription || fulfillsProjectHandleId) {
|
||||||
@@ -936,7 +932,7 @@ export async function createAndSubmitOffer(
|
|||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
return createAndSubmitClaim(
|
return createAndSubmitClaim(
|
||||||
vcClaim as OfferVerifiableCredential,
|
vcClaim as OfferClaim,
|
||||||
issuerDid,
|
issuerDid,
|
||||||
apiServer,
|
apiServer,
|
||||||
axios,
|
axios,
|
||||||
@@ -946,7 +942,7 @@ export async function createAndSubmitOffer(
|
|||||||
export async function editAndSubmitOffer(
|
export async function editAndSubmitOffer(
|
||||||
axios: Axios,
|
axios: Axios,
|
||||||
apiServer: string,
|
apiServer: string,
|
||||||
fullClaim: GenericCredWrapper<OfferVerifiableCredential>,
|
fullClaim: GenericCredWrapper<OfferClaim>,
|
||||||
issuerDid: string,
|
issuerDid: string,
|
||||||
itemDescription: string,
|
itemDescription: string,
|
||||||
amount?: number,
|
amount?: number,
|
||||||
@@ -969,7 +965,7 @@ export async function editAndSubmitOffer(
|
|||||||
fullClaim.id,
|
fullClaim.id,
|
||||||
);
|
);
|
||||||
return createAndSubmitClaim(
|
return createAndSubmitClaim(
|
||||||
vcClaim as OfferVerifiableCredential,
|
vcClaim as OfferClaim,
|
||||||
issuerDid,
|
issuerDid,
|
||||||
apiServer,
|
apiServer,
|
||||||
axios,
|
axios,
|
||||||
@@ -1005,11 +1001,12 @@ export async function createAndSubmitClaim(
|
|||||||
axios: Axios,
|
axios: Axios,
|
||||||
): Promise<CreateAndSubmitClaimResult> {
|
): Promise<CreateAndSubmitClaimResult> {
|
||||||
try {
|
try {
|
||||||
const vcPayload = {
|
const vcPayload: { vc: VerifiableCredentialClaim } = {
|
||||||
vc: {
|
vc: {
|
||||||
"@context": ["https://www.w3.org/2018/credentials/v1"],
|
"@context": "https://www.w3.org/2018/credentials/v1",
|
||||||
|
"@type": "VerifiableCredential",
|
||||||
type: ["VerifiableCredential"],
|
type: ["VerifiableCredential"],
|
||||||
credentialSubject: vcClaim,
|
credentialSubject: vcClaim as unknown as ClaimObject,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1043,7 +1040,7 @@ export async function createAndSubmitClaim(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function generateEndorserJwtUrlForAccount(
|
export async function generateEndorserJwtUrlForAccount(
|
||||||
account: KeyMeta,
|
account: KeyMetaMaybeWithPrivate,
|
||||||
isRegistered: boolean,
|
isRegistered: boolean,
|
||||||
givenName: string,
|
givenName: string,
|
||||||
profileImageUrl: string,
|
profileImageUrl: string,
|
||||||
@@ -1067,7 +1064,7 @@ export async function generateEndorserJwtUrlForAccount(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add the next key -- not recommended for the QR code for such a high resolution
|
// Add the next key -- not recommended for the QR code for such a high resolution
|
||||||
if (isContact) {
|
if (isContact && account.derivationPath && account.mnemonic) {
|
||||||
const newDerivPath = nextDerivationPath(account.derivationPath);
|
const newDerivPath = nextDerivationPath(account.derivationPath);
|
||||||
const nextPublicHex = deriveAddress(account.mnemonic, newDerivPath)[2];
|
const nextPublicHex = deriveAddress(account.mnemonic, newDerivPath)[2];
|
||||||
const nextPublicEncKey = Buffer.from(nextPublicHex, "hex");
|
const nextPublicEncKey = Buffer.from(nextPublicHex, "hex");
|
||||||
@@ -1089,7 +1086,11 @@ export async function createEndorserJwtForDid(
|
|||||||
expiresIn?: number,
|
expiresIn?: number,
|
||||||
) {
|
) {
|
||||||
const account = await retrieveFullyDecryptedAccount(issuerDid);
|
const account = await retrieveFullyDecryptedAccount(issuerDid);
|
||||||
return createEndorserJwtForKey(account as KeyMeta, payload, expiresIn);
|
return createEndorserJwtForKey(
|
||||||
|
account as KeyMetaWithPrivate,
|
||||||
|
payload,
|
||||||
|
expiresIn,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1186,102 +1187,118 @@ export const claimSpecialDescription = (
|
|||||||
identifiers: Array<string>,
|
identifiers: Array<string>,
|
||||||
contacts: Array<Contact>,
|
contacts: Array<Contact>,
|
||||||
) => {
|
) => {
|
||||||
let claim = record.claim;
|
let claim:
|
||||||
|
| GenericVerifiableCredential
|
||||||
|
| GenericCredWrapper<GenericVerifiableCredential> = record.claim;
|
||||||
if ("claim" in claim) {
|
if ("claim" in claim) {
|
||||||
|
// it's a nested GenericCredWrapper
|
||||||
claim = claim.claim as GenericVerifiableCredential;
|
claim = claim.claim as GenericVerifiableCredential;
|
||||||
}
|
}
|
||||||
|
|
||||||
const issuer = didInfo(record.issuer, activeDid, identifiers, contacts);
|
const issuer = didInfo(record.issuer, activeDid, identifiers, contacts);
|
||||||
const claimObj = claim as ClaimObject;
|
const type = claim["@type"] || "UnknownType";
|
||||||
const type = claimObj["@type"] || "UnknownType";
|
|
||||||
|
|
||||||
if (type === "AgreeAction") {
|
if (type === "AgreeAction") {
|
||||||
return (
|
return (
|
||||||
issuer +
|
issuer +
|
||||||
" agreed with " +
|
" agreed with " +
|
||||||
claimSummary(claimObj.object as GenericVerifiableCredential)
|
claimSummary(claim.object as GenericVerifiableCredential)
|
||||||
);
|
);
|
||||||
} else if (isAccept(claim)) {
|
} else if (isAccept(claim)) {
|
||||||
return (
|
return (
|
||||||
issuer +
|
issuer +
|
||||||
" accepted " +
|
" accepted " +
|
||||||
claimSummary(claimObj.object as GenericVerifiableCredential)
|
claimSummary(claim.object as GenericVerifiableCredential)
|
||||||
);
|
);
|
||||||
} else if (type === "GiveAction") {
|
} else if (type === "GiveAction") {
|
||||||
const giveClaim = claim as GiveVerifiableCredential;
|
const giveClaim = claim as GiveActionClaim;
|
||||||
const agent: Agent = giveClaim.agent || {
|
// @ts-expect-error because .did may be found in legacy data, before March 2023
|
||||||
identifier: undefined,
|
const legacyGiverDid = giveClaim.agent?.did;
|
||||||
did: undefined,
|
const giver = giveClaim.agent?.identifier || legacyGiverDid;
|
||||||
};
|
const giverInfo = didInfo(giver, activeDid, identifiers, contacts);
|
||||||
const agentDid = agent.did || agent.identifier;
|
let gaveAmount = giveClaim.object?.amountOfThisGood
|
||||||
const contactInfo = agentDid
|
? displayAmount(
|
||||||
? didInfo(agentDid, activeDid, identifiers, contacts)
|
giveClaim.object.unitCode as string,
|
||||||
: "someone";
|
giveClaim.object.amountOfThisGood as number,
|
||||||
const offering = giveClaim.object
|
)
|
||||||
? " " + claimSummary(giveClaim.object)
|
|
||||||
: "";
|
: "";
|
||||||
const recipient = giveClaim.participant?.identifier;
|
if (giveClaim.description) {
|
||||||
const recipientInfo = recipient
|
if (gaveAmount) {
|
||||||
? " to " + didInfo(recipient, activeDid, identifiers, contacts)
|
gaveAmount = gaveAmount + ", and also: ";
|
||||||
|
}
|
||||||
|
gaveAmount = gaveAmount + giveClaim.description;
|
||||||
|
}
|
||||||
|
if (!gaveAmount) {
|
||||||
|
gaveAmount = "something not described";
|
||||||
|
}
|
||||||
|
// @ts-expect-error because .did may be found in legacy data, before March 2023
|
||||||
|
const legacyRecipDid = giveClaim.recipient?.did;
|
||||||
|
const gaveRecipientId = giveClaim.recipient?.identifier || legacyRecipDid;
|
||||||
|
const gaveRecipientInfo = gaveRecipientId
|
||||||
|
? " to " + didInfo(gaveRecipientId, activeDid, identifiers, contacts)
|
||||||
: "";
|
: "";
|
||||||
return contactInfo + " gave" + offering + recipientInfo;
|
return giverInfo + " gave" + gaveRecipientInfo + ": " + gaveAmount;
|
||||||
} else if (type === "JoinAction") {
|
} else if (type === "JoinAction") {
|
||||||
const joinClaim = claim as ClaimObject;
|
const joinClaim = claim as JoinActionClaim;
|
||||||
const agent: Agent = joinClaim.agent || {
|
// @ts-expect-error because .did may be found in legacy data, before March 2023
|
||||||
identifier: undefined,
|
const legacyDid = joinClaim.agent?.did;
|
||||||
did: undefined,
|
const agent = joinClaim.agent?.identifier || legacyDid;
|
||||||
};
|
const contactInfo = didInfo(agent, activeDid, identifiers, contacts);
|
||||||
const agentDid = agent.did || agent.identifier;
|
|
||||||
const contactInfo = agentDid
|
let eventOrganizer =
|
||||||
? didInfo(agentDid, activeDid, identifiers, contacts)
|
joinClaim.event &&
|
||||||
: "someone";
|
joinClaim.event.organizer &&
|
||||||
const object = joinClaim.object as GenericVerifiableCredential;
|
joinClaim.event.organizer.name;
|
||||||
const objectInfo = object ? " " + claimSummary(object) : "";
|
eventOrganizer = eventOrganizer || "";
|
||||||
return contactInfo + " joined" + objectInfo;
|
let eventName = joinClaim.event && joinClaim.event.name;
|
||||||
|
eventName = eventName ? " " + eventName : "";
|
||||||
|
let fullEvent = eventOrganizer + eventName;
|
||||||
|
fullEvent = fullEvent ? " attended the " + fullEvent : "";
|
||||||
|
|
||||||
|
let eventDate = joinClaim.event && joinClaim.event.startTime;
|
||||||
|
eventDate = eventDate ? " at " + eventDate : "";
|
||||||
|
return contactInfo + fullEvent + eventDate;
|
||||||
} else if (isOffer(claim)) {
|
} else if (isOffer(claim)) {
|
||||||
const offerClaim = claim as OfferVerifiableCredential;
|
const offerClaim = claim as OfferClaim;
|
||||||
const agent: Agent = offerClaim.agent || {
|
const offerer = offerClaim.offeredBy?.identifier;
|
||||||
identifier: undefined,
|
const contactInfo = didInfo(offerer, activeDid, identifiers, contacts);
|
||||||
did: undefined,
|
let offering = "";
|
||||||
};
|
if (offerClaim.includesObject) {
|
||||||
const agentDid = agent.did || agent.identifier;
|
offering +=
|
||||||
const contactInfo = agentDid
|
" " +
|
||||||
? didInfo(agentDid, activeDid, identifiers, contacts)
|
displayAmount(
|
||||||
: "someone";
|
offerClaim.includesObject.unitCode,
|
||||||
const offering = offerClaim.object
|
offerClaim.includesObject.amountOfThisGood,
|
||||||
? " " + claimSummary(offerClaim.object)
|
);
|
||||||
: "";
|
}
|
||||||
const offerRecipientId = offerClaim.participant?.identifier;
|
if (offerClaim.itemOffered?.description) {
|
||||||
|
offering += ", saying: " + offerClaim.itemOffered?.description;
|
||||||
|
}
|
||||||
|
// @ts-expect-error because .did may be found in legacy data, before March 2023
|
||||||
|
const legacyDid = offerClaim.recipient?.did;
|
||||||
|
const offerRecipientId = offerClaim.recipient?.identifier || legacyDid;
|
||||||
const offerRecipientInfo = offerRecipientId
|
const offerRecipientInfo = offerRecipientId
|
||||||
? " to " + didInfo(offerRecipientId, activeDid, identifiers, contacts)
|
? " to " + didInfo(offerRecipientId, activeDid, identifiers, contacts)
|
||||||
: "";
|
: "";
|
||||||
return contactInfo + " offered" + offering + offerRecipientInfo;
|
return contactInfo + " offered" + offering + offerRecipientInfo;
|
||||||
} else if (type === "PlanAction") {
|
} else if (type === "PlanAction") {
|
||||||
const planClaim = claim as ClaimObject;
|
const planClaim = claim as PlanActionClaim;
|
||||||
const agent: Agent = planClaim.agent || {
|
const claimer = planClaim.agent?.identifier || record.issuer;
|
||||||
identifier: undefined,
|
const claimerInfo = didInfo(claimer, activeDid, identifiers, contacts);
|
||||||
did: undefined,
|
return claimerInfo + " announced a project: " + planClaim.name;
|
||||||
};
|
|
||||||
const agentDid = agent.did || agent.identifier;
|
|
||||||
const contactInfo = agentDid
|
|
||||||
? didInfo(agentDid, activeDid, identifiers, contacts)
|
|
||||||
: "someone";
|
|
||||||
const object = planClaim.object as GenericVerifiableCredential;
|
|
||||||
const objectInfo = object ? " " + claimSummary(object) : "";
|
|
||||||
return contactInfo + " planned" + objectInfo;
|
|
||||||
} else if (type === "Tenure") {
|
} else if (type === "Tenure") {
|
||||||
const tenureClaim = claim as ClaimObject;
|
const tenureClaim = claim as TenureClaim;
|
||||||
const agent: Agent = tenureClaim.agent || {
|
// @ts-expect-error because .did may be found in legacy data, before March 2023
|
||||||
identifier: undefined,
|
const legacyDid = tenureClaim.party?.did;
|
||||||
did: undefined,
|
const claimer = tenureClaim.party?.identifier || legacyDid;
|
||||||
};
|
const contactInfo = didInfo(claimer, activeDid, identifiers, contacts);
|
||||||
const agentDid = agent.did || agent.identifier;
|
const polygon = tenureClaim.spatialUnit?.geo?.polygon || "";
|
||||||
const contactInfo = agentDid
|
return (
|
||||||
? didInfo(agentDid, activeDid, identifiers, contacts)
|
contactInfo +
|
||||||
: "someone";
|
" possesses [" +
|
||||||
const object = tenureClaim.object as GenericVerifiableCredential;
|
polygon.substring(0, polygon.indexOf(" ")) +
|
||||||
const objectInfo = object ? " " + claimSummary(object) : "";
|
"...]"
|
||||||
return contactInfo + " has tenure" + objectInfo;
|
);
|
||||||
} else {
|
} else {
|
||||||
return issuer + " declared " + claimSummary(claim);
|
return issuer + " declared " + claimSummary(claim);
|
||||||
}
|
}
|
||||||
@@ -1315,7 +1332,7 @@ export async function createEndorserJwtVcFromClaim(
|
|||||||
// Make a payload for the claim
|
// Make a payload for the claim
|
||||||
const vcPayload = {
|
const vcPayload = {
|
||||||
vc: {
|
vc: {
|
||||||
"@context": ["https://www.w3.org/2018/credentials/v1"],
|
"@context": "https://www.w3.org/2018/credentials/v1",
|
||||||
type: ["VerifiableCredential"],
|
type: ["VerifiableCredential"],
|
||||||
credentialSubject: claim,
|
credentialSubject: claim,
|
||||||
},
|
},
|
||||||
@@ -1323,32 +1340,44 @@ export async function createEndorserJwtVcFromClaim(
|
|||||||
return createEndorserJwtForDid(issuerDid, vcPayload);
|
return createEndorserJwtForDid(issuerDid, vcPayload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a JWT for a RegisterAction claim.
|
||||||
|
*
|
||||||
|
* @param activeDid - The DID of the user creating the invite
|
||||||
|
* @param contact - The contact to register, with a 'did' field (all optional for invites)
|
||||||
|
* @param identifier - The identifier for the invite, usually random
|
||||||
|
* @param expiresIn - The number of seconds until the invite expires
|
||||||
|
* @returns The JWT for the RegisterAction claim
|
||||||
|
*/
|
||||||
export async function createInviteJwt(
|
export async function createInviteJwt(
|
||||||
activeDid: string,
|
activeDid: string,
|
||||||
contact: Contact,
|
contact?: Contact,
|
||||||
|
identifier?: string,
|
||||||
|
expiresIn?: number, // in seconds
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const vcClaim: RegisterVerifiableCredential = {
|
const vcClaim: RegisterActionClaim = {
|
||||||
"@context": SCHEMA_ORG_CONTEXT,
|
"@context": SCHEMA_ORG_CONTEXT,
|
||||||
"@type": "RegisterAction",
|
"@type": "RegisterAction",
|
||||||
agent: { identifier: activeDid },
|
agent: { identifier: activeDid },
|
||||||
object: SERVICE_ID,
|
object: SERVICE_ID,
|
||||||
|
identifier: identifier,
|
||||||
};
|
};
|
||||||
if (contact) {
|
if (contact?.did) {
|
||||||
vcClaim.participant = { identifier: contact.did };
|
vcClaim.participant = { identifier: contact.did };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make a payload for the claim
|
// Make a payload for the claim
|
||||||
const vcPayload: { vc: VerifiableCredentialClaim } = {
|
const vcPayload: { vc: VerifiableCredentialClaim } = {
|
||||||
vc: {
|
vc: {
|
||||||
"@context": ["https://www.w3.org/2018/credentials/v1"],
|
"@context": "https://www.w3.org/2018/credentials/v1",
|
||||||
"@type": "VerifiableCredential",
|
"@type": "VerifiableCredential",
|
||||||
type: ["VerifiableCredential"],
|
type: ["VerifiableCredential"],
|
||||||
credentialSubject: vcClaim as unknown as ClaimObject, // Type assertion needed due to object being string
|
credentialSubject: vcClaim as unknown as ClaimObject,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create a signature using private key of identity
|
// Create a signature using private key of identity
|
||||||
const vcJwt = await createEndorserJwtForDid(activeDid, vcPayload);
|
const vcJwt = await createEndorserJwtForDid(activeDid, vcPayload, expiresIn);
|
||||||
return vcJwt;
|
return vcJwt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
125
src/libs/util.ts
125
src/libs/util.ts
@@ -34,10 +34,10 @@ import { containsHiddenDid } from "../libs/endorserServer";
|
|||||||
import {
|
import {
|
||||||
GenericCredWrapper,
|
GenericCredWrapper,
|
||||||
GenericVerifiableCredential,
|
GenericVerifiableCredential,
|
||||||
|
KeyMetaWithPrivate,
|
||||||
} from "../interfaces/common";
|
} from "../interfaces/common";
|
||||||
import { GiveSummaryRecord } from "../interfaces/records";
|
import { GiveSummaryRecord } from "../interfaces/records";
|
||||||
import { OfferVerifiableCredential } from "../interfaces/claims";
|
import { OfferClaim } from "../interfaces/claims";
|
||||||
import { KeyMeta } from "../interfaces/common";
|
|
||||||
import { createPeerDid } from "../libs/crypto/vc/didPeer";
|
import { createPeerDid } from "../libs/crypto/vc/didPeer";
|
||||||
import { registerCredential } from "../libs/crypto/vc/passkeyDidPeer";
|
import { registerCredential } from "../libs/crypto/vc/passkeyDidPeer";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
@@ -378,17 +378,19 @@ export function base64ToBlob(base64DataUrl: string, sliceSize = 512) {
|
|||||||
* @param veriClaim is expected to have fields: claim and issuer
|
* @param veriClaim is expected to have fields: claim and issuer
|
||||||
*/
|
*/
|
||||||
export function offerGiverDid(
|
export function offerGiverDid(
|
||||||
veriClaim: GenericCredWrapper<GenericVerifiableCredential>,
|
veriClaim: GenericCredWrapper<OfferClaim>,
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
let giver;
|
const innerClaim = veriClaim.claim as OfferClaim;
|
||||||
const claim = veriClaim.claim as OfferVerifiableCredential;
|
let giver: string | undefined = undefined;
|
||||||
if (
|
|
||||||
claim.credentialSubject.offeredBy?.identifier &&
|
giver = innerClaim.offeredBy?.identifier;
|
||||||
!serverUtil.isHiddenDid(claim.credentialSubject.offeredBy.identifier)
|
if (giver && !serverUtil.isHiddenDid(giver)) {
|
||||||
) {
|
return giver;
|
||||||
giver = claim.credentialSubject.offeredBy.identifier;
|
}
|
||||||
} else if (veriClaim.issuer && !serverUtil.isHiddenDid(veriClaim.issuer)) {
|
|
||||||
giver = veriClaim.issuer;
|
giver = veriClaim.issuer;
|
||||||
|
if (giver && !serverUtil.isHiddenDid(giver)) {
|
||||||
|
return giver;
|
||||||
}
|
}
|
||||||
return giver;
|
return giver;
|
||||||
}
|
}
|
||||||
@@ -400,7 +402,10 @@ export function offerGiverDid(
|
|||||||
export const canFulfillOffer = (
|
export const canFulfillOffer = (
|
||||||
veriClaim: GenericCredWrapper<GenericVerifiableCredential>,
|
veriClaim: GenericCredWrapper<GenericVerifiableCredential>,
|
||||||
) => {
|
) => {
|
||||||
return veriClaim.claimType === "Offer" && !!offerGiverDid(veriClaim);
|
return (
|
||||||
|
veriClaim.claimType === "Offer" &&
|
||||||
|
!!offerGiverDid(veriClaim as GenericCredWrapper<OfferClaim>)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// return object with paths and arrays of DIDs for any keys ending in "VisibleToDid"
|
// return object with paths and arrays of DIDs for any keys ending in "VisibleToDid"
|
||||||
@@ -469,11 +474,7 @@ export function findAllVisibleToDids(
|
|||||||
*
|
*
|
||||||
**/
|
**/
|
||||||
|
|
||||||
export interface AccountKeyInfo
|
export type AccountKeyInfo = Account & KeyMetaWithPrivate;
|
||||||
extends Omit<Account, "derivationPath">,
|
|
||||||
Omit<KeyMeta, "derivationPath"> {
|
|
||||||
derivationPath?: string; // Make it optional to match Account type
|
|
||||||
}
|
|
||||||
|
|
||||||
export const retrieveAccountCount = async (): Promise<number> => {
|
export const retrieveAccountCount = async (): Promise<number> => {
|
||||||
let result = 0;
|
let result = 0;
|
||||||
@@ -510,12 +511,16 @@ export const retrieveAccountDids = async (): Promise<string[]> => {
|
|||||||
return allDids;
|
return allDids;
|
||||||
};
|
};
|
||||||
|
|
||||||
// This is provided and recommended when the full key is not necessary so that
|
/**
|
||||||
// future work could separate this info from the sensitive key material.
|
* This is provided and recommended when the full key is not necessary so that
|
||||||
|
* future work could separate this info from the sensitive key material.
|
||||||
|
*
|
||||||
|
* If you need the private key data, use retrieveFullyDecryptedAccount instead.
|
||||||
|
*/
|
||||||
export const retrieveAccountMetadata = async (
|
export const retrieveAccountMetadata = async (
|
||||||
activeDid: string,
|
activeDid: string,
|
||||||
): Promise<AccountKeyInfo | undefined> => {
|
): Promise<Account | undefined> => {
|
||||||
let result: AccountKeyInfo | undefined = undefined;
|
let result: Account | undefined = undefined;
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
const dbAccount = await platformService.dbQuery(
|
const dbAccount = await platformService.dbQuery(
|
||||||
`SELECT * FROM accounts WHERE did = ?`,
|
`SELECT * FROM accounts WHERE did = ?`,
|
||||||
@@ -547,32 +552,16 @@ export const retrieveAccountMetadata = async (
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const retrieveAllAccountsMetadata = async (): Promise<Account[]> => {
|
/**
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
* This contains sensitive data. If possible, use retrieveAccountMetadata instead.
|
||||||
const dbAccounts = await platformService.dbQuery(`SELECT * FROM accounts`);
|
*
|
||||||
const accounts = databaseUtil.mapQueryResultToValues(dbAccounts) as Account[];
|
* @param activeDid
|
||||||
let result = accounts.map((account) => {
|
* @returns account info with private key data decrypted
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
*/
|
||||||
const { identity, mnemonic, ...metadata } = account;
|
|
||||||
return metadata as Account;
|
|
||||||
});
|
|
||||||
if (USE_DEXIE_DB) {
|
|
||||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
|
||||||
const accountsDB = await accountsDBPromise;
|
|
||||||
const array = await accountsDB.accounts.toArray();
|
|
||||||
result = array.map((account) => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const { identity, mnemonic, ...metadata } = account;
|
|
||||||
return metadata as Account;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const retrieveFullyDecryptedAccount = async (
|
export const retrieveFullyDecryptedAccount = async (
|
||||||
activeDid: string,
|
activeDid: string,
|
||||||
): Promise<AccountKeyInfo | undefined> => {
|
): Promise<Account | undefined> => {
|
||||||
let result: AccountKeyInfo | undefined = undefined;
|
let result: Account | undefined = undefined;
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
const dbSecrets = await platformService.dbQuery(
|
const dbSecrets = await platformService.dbQuery(
|
||||||
`SELECT secretBase64 from secret`,
|
`SELECT secretBase64 from secret`,
|
||||||
@@ -620,20 +609,26 @@ export const retrieveFullyDecryptedAccount = async (
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
// let's try and eliminate this
|
export const retrieveAllAccountsMetadata = async (): Promise<Account[]> => {
|
||||||
export const retrieveAllFullyDecryptedAccounts = async (): Promise<
|
|
||||||
Array<AccountEncrypted>
|
|
||||||
> => {
|
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
const queryResult = await platformService.dbQuery("SELECT * FROM accounts");
|
const dbAccounts = await platformService.dbQuery(`SELECT * FROM accounts`);
|
||||||
let allAccounts = databaseUtil.mapQueryResultToValues(
|
const accounts = databaseUtil.mapQueryResultToValues(dbAccounts) as Account[];
|
||||||
queryResult,
|
let result = accounts.map((account) => {
|
||||||
) as unknown as AccountEncrypted[];
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { identity, mnemonic, ...metadata } = account;
|
||||||
|
return metadata as Account;
|
||||||
|
});
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
|
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||||
const accountsDB = await accountsDBPromise;
|
const accountsDB = await accountsDBPromise;
|
||||||
allAccounts = (await accountsDB.accounts.toArray()) as AccountEncrypted[];
|
const array = await accountsDB.accounts.toArray();
|
||||||
|
result = array.map((account) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { identity, mnemonic, ...metadata } = account;
|
||||||
|
return metadata as Account;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return allAccounts;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -651,17 +646,29 @@ export async function saveNewIdentity(
|
|||||||
const secrets = await platformService.dbQuery(
|
const secrets = await platformService.dbQuery(
|
||||||
`SELECT secretBase64 FROM secret`,
|
`SELECT secretBase64 FROM secret`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// If no secret exists, create one
|
||||||
|
let secretBase64: string;
|
||||||
if (!secrets?.values?.length || !secrets.values[0]?.length) {
|
if (!secrets?.values?.length || !secrets.values[0]?.length) {
|
||||||
throw new Error(
|
// Generate a new secret
|
||||||
"No initial encryption supported. We recommend you clear your data and start over.",
|
const randomBytes = crypto.getRandomValues(new Uint8Array(32));
|
||||||
|
secretBase64 = arrayBufferToBase64(randomBytes);
|
||||||
|
|
||||||
|
// Store the new secret
|
||||||
|
await platformService.dbExec(
|
||||||
|
`INSERT INTO secret (id, secretBase64) VALUES (1, ?)`,
|
||||||
|
[secretBase64],
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
secretBase64 = secrets.values[0][0] as string;
|
||||||
}
|
}
|
||||||
const secretBase64 = secrets.values[0][0] as string;
|
|
||||||
const secret = base64ToArrayBuffer(secretBase64);
|
const secret = base64ToArrayBuffer(secretBase64);
|
||||||
const encryptedIdentity = await simpleEncrypt(identity, secret);
|
const encryptedIdentity = await simpleEncrypt(identity, secret);
|
||||||
const encryptedMnemonic = await simpleEncrypt(mnemonic, secret);
|
const encryptedMnemonic = await simpleEncrypt(mnemonic, secret);
|
||||||
const encryptedIdentityBase64 = arrayBufferToBase64(encryptedIdentity);
|
const encryptedIdentityBase64 = arrayBufferToBase64(encryptedIdentity);
|
||||||
const encryptedMnemonicBase64 = arrayBufferToBase64(encryptedMnemonic);
|
const encryptedMnemonicBase64 = arrayBufferToBase64(encryptedMnemonic);
|
||||||
|
|
||||||
await platformService.dbExec(
|
await platformService.dbExec(
|
||||||
`INSERT INTO accounts (dateCreated, derivationPath, did, identityEncrBase64, mnemonicEncrBase64, publicKeyHex)
|
`INSERT INTO accounts (dateCreated, derivationPath, did, identityEncrBase64, mnemonicEncrBase64, publicKeyHex)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { createPinia } from "pinia";
|
|||||||
import { App as VueApp, ComponentPublicInstance, createApp } from "vue";
|
import { App as VueApp, ComponentPublicInstance, createApp } from "vue";
|
||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
import router from "./router";
|
import router from "./router";
|
||||||
|
// Use the browser version of axios for web builds
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import VueAxios from "vue-axios";
|
import VueAxios from "vue-axios";
|
||||||
import Notifications from "notiwind";
|
import Notifications from "notiwind";
|
||||||
@@ -13,8 +14,8 @@ import { logger } from "./utils/logger";
|
|||||||
const platform = process.env.VITE_PLATFORM;
|
const platform = process.env.VITE_PLATFORM;
|
||||||
const pwa_enabled = process.env.VITE_PWA_ENABLED === "true";
|
const pwa_enabled = process.env.VITE_PWA_ENABLED === "true";
|
||||||
|
|
||||||
logger.error("Platform", { platform });
|
logger.log("Platform", { platform });
|
||||||
logger.error("PWA enabled", { pwa_enabled });
|
logger.log("PWA enabled", { pwa_enabled });
|
||||||
|
|
||||||
// Global Error Handler
|
// Global Error Handler
|
||||||
function setupGlobalErrorHandler(app: VueApp) {
|
function setupGlobalErrorHandler(app: VueApp) {
|
||||||
|
|||||||
@@ -1,16 +1,301 @@
|
|||||||
import { initializeApp } from "./main.common";
|
import { initializeApp } from "./main.common";
|
||||||
import { logger } from "./utils/logger";
|
import { logger } from "./utils/logger";
|
||||||
|
import { SQLiteQueryResult } from "./services/platforms/ElectronPlatformService";
|
||||||
|
|
||||||
const platform = process.env.VITE_PLATFORM;
|
const platform = process.env.VITE_PLATFORM;
|
||||||
const pwa_enabled = process.env.VITE_PWA_ENABLED === "true";
|
const pwa_enabled = process.env.VITE_PWA_ENABLED === "true";
|
||||||
|
|
||||||
logger.info("[Electron] Initializing app");
|
logger.info("[Main Electron] Initializing app");
|
||||||
logger.info("[Electron] Platform:", { platform });
|
logger.info("[Main Electron] Platform:", { platform });
|
||||||
logger.info("[Electron] PWA enabled:", { pwa_enabled });
|
logger.info("[Main Electron] PWA enabled:", { pwa_enabled });
|
||||||
|
|
||||||
if (pwa_enabled) {
|
if (pwa_enabled) {
|
||||||
logger.warn("[Electron] PWA is enabled, but not supported in electron");
|
logger.warn("[Main Electron] PWA is enabled, but not supported in electron");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize app and SQLite
|
||||||
const app = initializeApp();
|
const app = initializeApp();
|
||||||
app.mount("#app");
|
|
||||||
|
// Create a promise that resolves when SQLite is ready
|
||||||
|
const sqliteReady = new Promise<void>((resolve, reject) => {
|
||||||
|
let retryCount = 0;
|
||||||
|
let initializationTimeout: NodeJS.Timeout;
|
||||||
|
|
||||||
|
const attemptInitialization = () => {
|
||||||
|
// Clear any existing timeout
|
||||||
|
if (initializationTimeout) {
|
||||||
|
clearTimeout(initializationTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set timeout for this attempt
|
||||||
|
initializationTimeout = setTimeout(() => {
|
||||||
|
if (retryCount < 3) {
|
||||||
|
// Use same retry count as ElectronPlatformService
|
||||||
|
retryCount++;
|
||||||
|
logger.warn(
|
||||||
|
`[Main Electron] SQLite initialization attempt ${retryCount} timed out, retrying...`,
|
||||||
|
);
|
||||||
|
setTimeout(attemptInitialization, 1000); // Use same delay as ElectronPlatformService
|
||||||
|
} else {
|
||||||
|
logger.error(
|
||||||
|
"[Main Electron] SQLite initialization failed after all retries",
|
||||||
|
);
|
||||||
|
reject(new Error("SQLite initialization timeout after all retries"));
|
||||||
|
}
|
||||||
|
}, 10000); // Use same timeout as ElectronPlatformService
|
||||||
|
|
||||||
|
// Wait for electron bridge to be available
|
||||||
|
const checkElectronBridge = () => {
|
||||||
|
if (!window.electron?.ipcRenderer) {
|
||||||
|
// Check again in 100ms if bridge isn't ready
|
||||||
|
setTimeout(checkElectronBridge, 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point we know ipcRenderer exists
|
||||||
|
const ipcRenderer = window.electron.ipcRenderer;
|
||||||
|
|
||||||
|
logger.info("[Main Electron] [IPC:bridge] IPC renderer bridge available");
|
||||||
|
|
||||||
|
// Listen for SQLite ready signal
|
||||||
|
logger.debug(
|
||||||
|
"[Main Electron] [IPC:sqlite-ready] Registering listener for SQLite ready signal",
|
||||||
|
);
|
||||||
|
ipcRenderer.once("sqlite-ready", () => {
|
||||||
|
clearTimeout(initializationTimeout);
|
||||||
|
logger.info(
|
||||||
|
"[Main Electron] [IPC:sqlite-ready] Received SQLite ready signal",
|
||||||
|
);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also listen for database errors
|
||||||
|
logger.debug(
|
||||||
|
"[Main Electron] [IPC:database-status] Registering listener for database status",
|
||||||
|
);
|
||||||
|
ipcRenderer.once("database-status", (...args: unknown[]) => {
|
||||||
|
clearTimeout(initializationTimeout);
|
||||||
|
const status = args[0] as { status: string; error?: string };
|
||||||
|
if (status.status === "error") {
|
||||||
|
logger.error(
|
||||||
|
"[Main Electron] [IPC:database-status] Database error:",
|
||||||
|
{
|
||||||
|
error: status.error,
|
||||||
|
channel: "database-status",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
reject(new Error(status.error || "Database initialization failed"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if SQLite is already available
|
||||||
|
logger.debug(
|
||||||
|
"[Main Electron] [IPC:sqlite-is-available] Checking SQLite availability",
|
||||||
|
);
|
||||||
|
ipcRenderer
|
||||||
|
.invoke("sqlite-is-available")
|
||||||
|
.then(async (result: unknown) => {
|
||||||
|
const isAvailable = Boolean(result);
|
||||||
|
if (isAvailable) {
|
||||||
|
logger.info(
|
||||||
|
"[Main Electron] [IPC:sqlite-is-available] SQLite is available",
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// First create a database connection
|
||||||
|
logger.debug(
|
||||||
|
"[Main Electron] [IPC:get-path] Requesting database path",
|
||||||
|
);
|
||||||
|
const dbPath = await ipcRenderer.invoke("get-path");
|
||||||
|
logger.info(
|
||||||
|
"[Main Electron] [IPC:get-path] Database path received:",
|
||||||
|
{ dbPath },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create the database connection
|
||||||
|
logger.debug(
|
||||||
|
"[Main Electron] [IPC:sqlite-create-connection] Creating database connection",
|
||||||
|
);
|
||||||
|
await ipcRenderer.invoke("sqlite-create-connection", {
|
||||||
|
database: "timesafari",
|
||||||
|
version: 1,
|
||||||
|
});
|
||||||
|
logger.info(
|
||||||
|
"[Main Electron] [IPC:sqlite-create-connection] Database connection created",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Explicitly open the database
|
||||||
|
logger.debug(
|
||||||
|
"[Main Electron] [IPC:sqlite-open] Opening database",
|
||||||
|
);
|
||||||
|
await ipcRenderer.invoke("sqlite-open", {
|
||||||
|
database: "timesafari",
|
||||||
|
});
|
||||||
|
logger.info(
|
||||||
|
"[Main Electron] [IPC:sqlite-open] Database opened successfully",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify the database is open
|
||||||
|
logger.debug(
|
||||||
|
"[Main Electron] [IPC:sqlite-is-db-open] Verifying database is open",
|
||||||
|
);
|
||||||
|
const isOpen = await ipcRenderer.invoke("sqlite-is-db-open", {
|
||||||
|
database: "timesafari",
|
||||||
|
});
|
||||||
|
logger.info(
|
||||||
|
"[Main Electron] [IPC:sqlite-is-db-open] Database open status:",
|
||||||
|
{ isOpen },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isOpen) {
|
||||||
|
throw new Error("Database failed to open");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now execute the test query
|
||||||
|
logger.debug(
|
||||||
|
"[Main Electron] [IPC:sqlite-query] Executing test query",
|
||||||
|
);
|
||||||
|
const testQuery = (await ipcRenderer.invoke("sqlite-query", {
|
||||||
|
database: "timesafari",
|
||||||
|
statement: "SELECT 1 as test;", // Safe test query
|
||||||
|
})) as SQLiteQueryResult;
|
||||||
|
logger.info(
|
||||||
|
"[Main Electron] [IPC:sqlite-query] Test query successful:",
|
||||||
|
{
|
||||||
|
hasResults: Boolean(testQuery?.values),
|
||||||
|
resultCount: testQuery?.values?.length,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Signal that SQLite is ready - database stays open
|
||||||
|
logger.debug(
|
||||||
|
"[Main Electron] [IPC:sqlite-status] Sending SQLite ready status",
|
||||||
|
);
|
||||||
|
await ipcRenderer.invoke("sqlite-status", {
|
||||||
|
status: "ready",
|
||||||
|
database: "timesafari",
|
||||||
|
timestamp: Date.now(),
|
||||||
|
});
|
||||||
|
logger.info(
|
||||||
|
"[Main Electron] SQLite ready status sent, database connection maintained",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove the close operations - database stays open for component use
|
||||||
|
// Database will be closed during app shutdown
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
"[Main Electron] [IPC:*] SQLite test operation failed:",
|
||||||
|
{
|
||||||
|
error,
|
||||||
|
lastOperation: "sqlite-test-query",
|
||||||
|
database: "timesafari",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Try to close everything if anything was opened
|
||||||
|
try {
|
||||||
|
logger.debug(
|
||||||
|
"[Main Electron] [IPC:cleanup] Attempting database cleanup after error",
|
||||||
|
);
|
||||||
|
await ipcRenderer
|
||||||
|
.invoke("sqlite-close", {
|
||||||
|
database: "timesafari",
|
||||||
|
})
|
||||||
|
.catch((closeError) => {
|
||||||
|
logger.warn(
|
||||||
|
"[Main Electron] [IPC:sqlite-close] Failed to close database during cleanup:",
|
||||||
|
closeError,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await ipcRenderer
|
||||||
|
.invoke("sqlite-close-connection", {
|
||||||
|
database: "timesafari",
|
||||||
|
})
|
||||||
|
.catch((closeError) => {
|
||||||
|
logger.warn(
|
||||||
|
"[Main Electron] [IPC:sqlite-close-connection] Failed to close connection during cleanup:",
|
||||||
|
closeError,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"[Main Electron] [IPC:cleanup] Database cleanup completed after error",
|
||||||
|
);
|
||||||
|
} catch (closeError) {
|
||||||
|
logger.error(
|
||||||
|
"[Main Electron] [IPC:cleanup] Failed to cleanup database:",
|
||||||
|
{
|
||||||
|
error: closeError,
|
||||||
|
database: "timesafari",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Don't reject here - we still want to wait for the ready signal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error: Error) => {
|
||||||
|
logger.error(
|
||||||
|
"[Main Electron] [IPC:sqlite-is-available] Failed to check SQLite availability:",
|
||||||
|
{
|
||||||
|
error,
|
||||||
|
channel: "sqlite-is-available",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// Don't reject here - wait for either ready signal or timeout
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start checking for bridge
|
||||||
|
checkElectronBridge();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start first initialization attempt
|
||||||
|
attemptInitialization();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for SQLite to be ready before initializing router and mounting app
|
||||||
|
sqliteReady
|
||||||
|
.then(async () => {
|
||||||
|
logger.info("[Main Electron] SQLite ready, initializing router...");
|
||||||
|
|
||||||
|
// Initialize router after SQLite is ready
|
||||||
|
const router = await import("./router").then((m) => m.default);
|
||||||
|
app.use(router);
|
||||||
|
logger.info("[Main Electron] Router initialized");
|
||||||
|
|
||||||
|
// Now mount the app
|
||||||
|
logger.info("[Main Electron] Mounting app...");
|
||||||
|
app.mount("#app");
|
||||||
|
logger.info("[Main Electron] App mounted successfully");
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error(
|
||||||
|
"[Main Electron] Failed to initialize SQLite:",
|
||||||
|
error instanceof Error ? error.message : "Unknown error",
|
||||||
|
);
|
||||||
|
// Show error to user with retry option
|
||||||
|
const errorDiv = document.createElement("div");
|
||||||
|
errorDiv.style.cssText =
|
||||||
|
"position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #ffebee; color: #c62828; padding: 20px; border-radius: 4px; text-align: center; max-width: 80%; z-index: 9999;";
|
||||||
|
errorDiv.innerHTML = `
|
||||||
|
<h2>Failed to Initialize Application</h2>
|
||||||
|
<p>There was an error initializing the database. This could be due to:</p>
|
||||||
|
<ul style="text-align: left; margin: 10px 0;">
|
||||||
|
<li>Database file is locked by another process</li>
|
||||||
|
<li>Insufficient permissions to access the database</li>
|
||||||
|
<li>Database file is corrupted</li>
|
||||||
|
</ul>
|
||||||
|
<p>Error details: ${error instanceof Error ? error.message : "Unknown error"}</p>
|
||||||
|
<div style="margin-top: 15px;">
|
||||||
|
<button onclick="window.location.reload()" style="margin: 0 5px; padding: 8px 16px; background: #c62828; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
<button onclick="window.electron.ipcRenderer.send('sqlite-status', { action: 'reset' })" style="margin: 0 5px; padding: 8px 16px; background: #f57c00; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||||||
|
Reset Database
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
document.body.appendChild(errorDiv);
|
||||||
|
});
|
||||||
|
|||||||
@@ -277,18 +277,31 @@ const initialPath = isElectron
|
|||||||
? window.location.pathname.split("/dist-electron/www/")[1] || "/"
|
? window.location.pathname.split("/dist-electron/www/")[1] || "/"
|
||||||
: window.location.pathname;
|
: window.location.pathname;
|
||||||
|
|
||||||
|
logger.info("[Router] Initializing router", { isElectron, initialPath });
|
||||||
|
|
||||||
const history = isElectron
|
const history = isElectron
|
||||||
? createMemoryHistory() // Memory history for Electron
|
? createMemoryHistory() // Memory history for Electron
|
||||||
: createWebHistory("/"); // Add base path for web apps
|
: createWebHistory("/"); // Add base path for web apps
|
||||||
|
|
||||||
/** @type {*} */
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history,
|
history,
|
||||||
routes,
|
routes,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set initial route
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
logger.info("[Router] Navigation", { to: to.path, from: from.path });
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
// Replace initial URL to start at `/` if necessary
|
// Replace initial URL to start at `/` if necessary
|
||||||
router.replace(initialPath || "/");
|
if (initialPath === "/" || !initialPath) {
|
||||||
|
logger.info("[Router] Setting initial route to /");
|
||||||
|
router.replace("/");
|
||||||
|
} else {
|
||||||
|
logger.info("[Router] Setting initial route to", initialPath);
|
||||||
|
router.replace(initialPath);
|
||||||
|
}
|
||||||
|
|
||||||
const errorHandler = (
|
const errorHandler = (
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ class AbsurdSqlDatabaseService implements DatabaseService {
|
|||||||
SQL.FS.mkdir("/sql");
|
SQL.FS.mkdir("/sql");
|
||||||
SQL.FS.mount(sqlFS, {}, "/sql");
|
SQL.FS.mount(sqlFS, {}, "/sql");
|
||||||
|
|
||||||
const path = "/sql/timesafari.sqlite";
|
const path = "/sql/timesafari.absurd-sql";
|
||||||
if (typeof SharedArrayBuffer === "undefined") {
|
if (typeof SharedArrayBuffer === "undefined") {
|
||||||
const stream = SQL.FS.open(path, "a+");
|
const stream = SQL.FS.open(path, "a+");
|
||||||
await stream.node.contents.readIfFallback();
|
await stream.node.contents.readIfFallback();
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
import { QueryExecResult } from "@/interfaces/database";
|
import { QueryExecResult } from "@/interfaces/database";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query execution result interface
|
||||||
|
*/
|
||||||
|
export interface QueryExecResult<T = unknown> {
|
||||||
|
columns: string[];
|
||||||
|
values: T[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the result of an image capture or selection operation.
|
* Represents the result of an image capture or selection operation.
|
||||||
* Contains both the image data as a Blob and the associated filename.
|
* Contains both the image data as a Blob and the associated filename.
|
||||||
@@ -102,15 +110,15 @@ export interface PlatformService {
|
|||||||
handleDeepLink(url: string): Promise<void>;
|
handleDeepLink(url: string): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes a SQL query on the database.
|
* Execute a database query and return the results
|
||||||
* @param sql - The SQL query to execute
|
* @param sql SQL query to execute
|
||||||
* @param params - The parameters to pass to the query
|
* @param params Query parameters
|
||||||
* @returns Promise resolving to the query result
|
* @returns Query results with columns and values
|
||||||
*/
|
*/
|
||||||
dbQuery(
|
dbQuery<T = unknown>(
|
||||||
sql: string,
|
sql: string,
|
||||||
params?: unknown[],
|
params?: unknown[],
|
||||||
): Promise<QueryExecResult | undefined>;
|
): Promise<QueryExecResult<T>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes a create/update/delete on the database.
|
* Executes a create/update/delete on the database.
|
||||||
|
|||||||
132
src/services/database/ConnectionPool.ts
Normal file
132
src/services/database/ConnectionPool.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import { logger } from "../../utils/logger";
|
||||||
|
import { SQLiteDBConnection } from "@capacitor-community/sqlite";
|
||||||
|
|
||||||
|
interface ConnectionState {
|
||||||
|
connection: SQLiteDBConnection;
|
||||||
|
lastUsed: number;
|
||||||
|
inUse: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DatabaseConnectionPool {
|
||||||
|
private static instance: DatabaseConnectionPool | null = null;
|
||||||
|
private connections: Map<string, ConnectionState> = new Map();
|
||||||
|
private readonly MAX_CONNECTIONS = 1; // We only need one connection for SQLite
|
||||||
|
private readonly MAX_IDLE_TIME = 5 * 60 * 1000; // 5 minutes
|
||||||
|
private readonly CLEANUP_INTERVAL = 60 * 1000; // 1 minute
|
||||||
|
private cleanupInterval: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
// Start cleanup interval
|
||||||
|
this.cleanupInterval = setInterval(
|
||||||
|
() => this.cleanup(),
|
||||||
|
this.CLEANUP_INTERVAL,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getInstance(): DatabaseConnectionPool {
|
||||||
|
if (!DatabaseConnectionPool.instance) {
|
||||||
|
DatabaseConnectionPool.instance = new DatabaseConnectionPool();
|
||||||
|
}
|
||||||
|
return DatabaseConnectionPool.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getConnection(
|
||||||
|
dbName: string,
|
||||||
|
createConnection: () => Promise<SQLiteDBConnection>,
|
||||||
|
): Promise<SQLiteDBConnection> {
|
||||||
|
// Check if we have an existing connection
|
||||||
|
const existing = this.connections.get(dbName);
|
||||||
|
if (existing && !existing.inUse) {
|
||||||
|
existing.inUse = true;
|
||||||
|
existing.lastUsed = Date.now();
|
||||||
|
logger.debug(
|
||||||
|
`[ConnectionPool] Reusing existing connection for ${dbName}`,
|
||||||
|
);
|
||||||
|
return existing.connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have too many connections, wait for one to be released
|
||||||
|
if (this.connections.size >= this.MAX_CONNECTIONS) {
|
||||||
|
logger.debug(`[ConnectionPool] Waiting for connection to be released...`);
|
||||||
|
await this.waitForConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new connection
|
||||||
|
try {
|
||||||
|
const connection = await createConnection();
|
||||||
|
this.connections.set(dbName, {
|
||||||
|
connection,
|
||||||
|
lastUsed: Date.now(),
|
||||||
|
inUse: true,
|
||||||
|
});
|
||||||
|
logger.debug(`[ConnectionPool] Created new connection for ${dbName}`);
|
||||||
|
return connection;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`[ConnectionPool] Failed to create connection for ${dbName}:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async releaseConnection(dbName: string): Promise<void> {
|
||||||
|
const connection = this.connections.get(dbName);
|
||||||
|
if (connection) {
|
||||||
|
connection.inUse = false;
|
||||||
|
connection.lastUsed = Date.now();
|
||||||
|
logger.debug(`[ConnectionPool] Released connection for ${dbName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async waitForConnection(): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const checkInterval = setInterval(() => {
|
||||||
|
if (this.connections.size < this.MAX_CONNECTIONS) {
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async cleanup(): Promise<void> {
|
||||||
|
const now = Date.now();
|
||||||
|
for (const [dbName, state] of this.connections.entries()) {
|
||||||
|
if (!state.inUse && now - state.lastUsed > this.MAX_IDLE_TIME) {
|
||||||
|
try {
|
||||||
|
await state.connection.close();
|
||||||
|
this.connections.delete(dbName);
|
||||||
|
logger.debug(
|
||||||
|
`[ConnectionPool] Cleaned up idle connection for ${dbName}`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(
|
||||||
|
`[ConnectionPool] Error closing idle connection for ${dbName}:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async closeAll(): Promise<void> {
|
||||||
|
if (this.cleanupInterval) {
|
||||||
|
clearInterval(this.cleanupInterval);
|
||||||
|
this.cleanupInterval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [dbName, state] of this.connections.entries()) {
|
||||||
|
try {
|
||||||
|
await state.connection.close();
|
||||||
|
logger.debug(`[ConnectionPool] Closed connection for ${dbName}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(
|
||||||
|
`[ConnectionPool] Error closing connection for ${dbName}:`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.connections.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,22 +2,97 @@ import {
|
|||||||
ImageResult,
|
ImageResult,
|
||||||
PlatformService,
|
PlatformService,
|
||||||
PlatformCapabilities,
|
PlatformCapabilities,
|
||||||
|
QueryExecResult,
|
||||||
} from "../PlatformService";
|
} from "../PlatformService";
|
||||||
import { logger } from "../../utils/logger";
|
import { logger } from "../../utils/logger";
|
||||||
import { QueryExecResult, SqlValue } from "@/interfaces/database";
|
|
||||||
import {
|
|
||||||
SQLiteConnection,
|
|
||||||
SQLiteDBConnection,
|
|
||||||
CapacitorSQLite,
|
|
||||||
Changes,
|
|
||||||
} from "@capacitor-community/sqlite";
|
|
||||||
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
|
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
|
||||||
|
import { ElectronAPI } from "../../utils/debug-electron";
|
||||||
|
|
||||||
|
import {
|
||||||
|
verifyElectronAPI,
|
||||||
|
testSQLiteOperations,
|
||||||
|
} from "../../utils/debug-electron";
|
||||||
|
|
||||||
|
// Extend the global Window interface
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
electron: ElectronAPI;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface Migration {
|
interface Migration {
|
||||||
name: string;
|
name: string;
|
||||||
sql: string;
|
sql: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Define the SQLite query result type
|
||||||
|
interface SQLiteQueryResult {
|
||||||
|
changes: number;
|
||||||
|
lastId: number;
|
||||||
|
rows?: unknown[];
|
||||||
|
values?: Record<string, unknown>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the QueryExecResult type to include success and changes
|
||||||
|
interface ElectronQueryExecResult {
|
||||||
|
success: boolean;
|
||||||
|
changes: number;
|
||||||
|
lastId?: number;
|
||||||
|
rows?: unknown[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared SQLite initialization state
|
||||||
|
* Used to coordinate initialization between main and service
|
||||||
|
*
|
||||||
|
* @author Matthew Raymer
|
||||||
|
*/
|
||||||
|
export interface SQLiteInitState {
|
||||||
|
isReady: boolean;
|
||||||
|
isInitializing: boolean;
|
||||||
|
error?: Error;
|
||||||
|
lastReadyCheck?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Singleton instance for shared state
|
||||||
|
const sqliteInitState: SQLiteInitState = {
|
||||||
|
isReady: false,
|
||||||
|
isInitializing: false,
|
||||||
|
lastReadyCheck: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface defining SQLite database operations
|
||||||
|
* @author Matthew Raymer
|
||||||
|
*/
|
||||||
|
interface SQLiteOperations {
|
||||||
|
createConnection: (options: {
|
||||||
|
database: string;
|
||||||
|
encrypted: boolean;
|
||||||
|
mode: string;
|
||||||
|
}) => Promise<void>;
|
||||||
|
query: (options: {
|
||||||
|
database: string;
|
||||||
|
statement: string;
|
||||||
|
values?: unknown[];
|
||||||
|
}) => Promise<{ values?: unknown[] }>;
|
||||||
|
execute: (options: { database: string; statements: string }) => Promise<void>;
|
||||||
|
run: (options: {
|
||||||
|
database: string;
|
||||||
|
statement: string;
|
||||||
|
values?: unknown[];
|
||||||
|
}) => Promise<{ changes?: { changes: number; lastId?: number } }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add at the top of the file after imports
|
||||||
|
const formatLogObject = (obj: unknown): string => {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(obj, null, 2);
|
||||||
|
} catch (error) {
|
||||||
|
return `[Object could not be stringified: ${error instanceof Error ? error.message : String(error)}]`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Platform service implementation for Electron (desktop) platform.
|
* Platform service implementation for Electron (desktop) platform.
|
||||||
* Provides native desktop functionality through Electron and Capacitor plugins for:
|
* Provides native desktop functionality through Electron and Capacitor plugins for:
|
||||||
@@ -25,68 +100,299 @@ interface Migration {
|
|||||||
* - Camera integration (TODO)
|
* - Camera integration (TODO)
|
||||||
* - SQLite database operations
|
* - SQLite database operations
|
||||||
* - System-level features (TODO)
|
* - System-level features (TODO)
|
||||||
|
*
|
||||||
|
* @author Matthew Raymer
|
||||||
*/
|
*/
|
||||||
export class ElectronPlatformService implements PlatformService {
|
export class ElectronPlatformService implements PlatformService {
|
||||||
private sqlite: SQLiteConnection;
|
private sqlite: SQLiteOperations | null = null;
|
||||||
private db: SQLiteDBConnection | null = null;
|
private dbName = "timesafari";
|
||||||
private dbName = "timesafari.db";
|
private isInitialized = false;
|
||||||
private initialized = false;
|
private dbFatalError = false;
|
||||||
|
private sqliteReadyPromise: Promise<void> | null = null;
|
||||||
|
private initializationTimeout: NodeJS.Timeout | null = null;
|
||||||
|
private isConnectionOpen = false;
|
||||||
|
private operationQueue: Promise<unknown> = Promise.resolve();
|
||||||
|
private queueLock = false;
|
||||||
|
private connectionState:
|
||||||
|
| "disconnected"
|
||||||
|
| "connecting"
|
||||||
|
| "connected"
|
||||||
|
| "error" = "disconnected";
|
||||||
|
private connectionPromise: Promise<void> | null = null;
|
||||||
|
|
||||||
|
// SQLite initialization configuration
|
||||||
|
private static readonly SQLITE_CONFIG = {
|
||||||
|
INITIALIZATION: {
|
||||||
|
TIMEOUT_MS: 5000, // Increase timeout to 5 seconds
|
||||||
|
RETRY_ATTEMPTS: 3,
|
||||||
|
RETRY_DELAY_MS: 1000,
|
||||||
|
READY_CHECK_INTERVAL_MS: 100,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.sqlite = new SQLiteConnection(CapacitorSQLite);
|
this.sqliteReadyPromise = new Promise<void>((resolve, reject) => {
|
||||||
|
let retryCount = 0;
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
if (this.initializationTimeout) {
|
||||||
|
clearTimeout(this.initializationTimeout);
|
||||||
|
this.initializationTimeout = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkExistingReadiness = async (): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
if (!window.electron?.ipcRenderer) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if SQLite is already available
|
||||||
|
const isAvailable = await window.electron.ipcRenderer.invoke(
|
||||||
|
"sqlite-is-available",
|
||||||
|
);
|
||||||
|
if (!isAvailable) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if database is already open
|
||||||
|
const isOpen = await window.electron.ipcRenderer.invoke(
|
||||||
|
"sqlite-is-db-open",
|
||||||
|
{
|
||||||
|
database: this.dbName,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
logger.info(
|
||||||
|
"[ElectronPlatformService] SQLite is already ready and database is open",
|
||||||
|
);
|
||||||
|
sqliteInitState.isReady = true;
|
||||||
|
sqliteInitState.isInitializing = false;
|
||||||
|
sqliteInitState.lastReadyCheck = Date.now();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(
|
||||||
|
"[ElectronPlatformService] Error checking existing readiness:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const attemptInitialization = async () => {
|
||||||
|
cleanup();
|
||||||
|
|
||||||
|
// Check if SQLite is already ready
|
||||||
|
if (await checkExistingReadiness()) {
|
||||||
|
this.isInitialized = true;
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If someone else is initializing, wait for them
|
||||||
|
if (sqliteInitState.isInitializing) {
|
||||||
|
logger.info(
|
||||||
|
"[ElectronPlatformService] Another initialization in progress, waiting...",
|
||||||
|
);
|
||||||
|
setTimeout(
|
||||||
|
attemptInitialization,
|
||||||
|
ElectronPlatformService.SQLITE_CONFIG.INITIALIZATION
|
||||||
|
.READY_CHECK_INTERVAL_MS,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
sqliteInitState.isInitializing = true;
|
||||||
|
|
||||||
|
// Verify Electron API exposure first
|
||||||
|
await verifyElectronAPI();
|
||||||
|
logger.info(
|
||||||
|
"[ElectronPlatformService] Electron API verification successful",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!window.electron?.ipcRenderer) {
|
||||||
|
logger.warn("[ElectronPlatformService] IPC renderer not available");
|
||||||
|
reject(new Error("IPC renderer not available"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up ready signal handler BEFORE setting timeout
|
||||||
|
window.electron.ipcRenderer.once("sqlite-ready", async () => {
|
||||||
|
cleanup();
|
||||||
|
logger.info(
|
||||||
|
"[ElectronPlatformService] Received SQLite ready signal",
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Test SQLite operations after receiving ready signal
|
||||||
|
await testSQLiteOperations();
|
||||||
|
logger.info(
|
||||||
|
"[ElectronPlatformService] SQLite operations test successful",
|
||||||
|
);
|
||||||
|
|
||||||
|
this.isInitialized = true;
|
||||||
|
sqliteInitState.isReady = true;
|
||||||
|
sqliteInitState.isInitializing = false;
|
||||||
|
sqliteInitState.lastReadyCheck = Date.now();
|
||||||
|
resolve();
|
||||||
|
} catch (error) {
|
||||||
|
sqliteInitState.error = error as Error;
|
||||||
|
sqliteInitState.isInitializing = false;
|
||||||
|
logger.error(
|
||||||
|
"[ElectronPlatformService] SQLite operations test failed:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up error handler
|
||||||
|
window.electron.ipcRenderer.once(
|
||||||
|
"database-status",
|
||||||
|
(...args: unknown[]) => {
|
||||||
|
cleanup();
|
||||||
|
const status = args[0] as { status: string; error?: string };
|
||||||
|
if (status.status === "error") {
|
||||||
|
this.dbFatalError = true;
|
||||||
|
sqliteInitState.error = new Error(
|
||||||
|
status.error || "Database initialization failed",
|
||||||
|
);
|
||||||
|
sqliteInitState.isInitializing = false;
|
||||||
|
reject(sqliteInitState.error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set timeout for this attempt AFTER setting up handlers
|
||||||
|
this.initializationTimeout = setTimeout(() => {
|
||||||
|
if (
|
||||||
|
retryCount <
|
||||||
|
ElectronPlatformService.SQLITE_CONFIG.INITIALIZATION
|
||||||
|
.RETRY_ATTEMPTS
|
||||||
|
) {
|
||||||
|
retryCount++;
|
||||||
|
logger.warn(
|
||||||
|
`[ElectronPlatformService] SQLite initialization attempt ${retryCount} timed out, retrying...`,
|
||||||
|
);
|
||||||
|
setTimeout(
|
||||||
|
attemptInitialization,
|
||||||
|
ElectronPlatformService.SQLITE_CONFIG.INITIALIZATION
|
||||||
|
.RETRY_DELAY_MS,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
cleanup();
|
||||||
|
sqliteInitState.isInitializing = false;
|
||||||
|
sqliteInitState.error = new Error(
|
||||||
|
"SQLite initialization timeout after all retries",
|
||||||
|
);
|
||||||
|
logger.error(
|
||||||
|
"[ElectronPlatformService] SQLite initialization failed after all retries",
|
||||||
|
);
|
||||||
|
reject(sqliteInitState.error);
|
||||||
|
}
|
||||||
|
}, ElectronPlatformService.SQLITE_CONFIG.INITIALIZATION.TIMEOUT_MS);
|
||||||
|
} catch (error) {
|
||||||
|
cleanup();
|
||||||
|
sqliteInitState.error = error as Error;
|
||||||
|
sqliteInitState.isInitializing = false;
|
||||||
|
logger.error(
|
||||||
|
"[ElectronPlatformService] Initialization failed:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start first initialization attempt
|
||||||
|
attemptInitialization();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async initializeDatabase(): Promise<void> {
|
private async initializeDatabase(): Promise<void> {
|
||||||
if (this.initialized) {
|
if (this.isInitialized) return;
|
||||||
return;
|
if (this.sqliteReadyPromise) await this.sqliteReadyPromise;
|
||||||
|
|
||||||
|
if (!window.electron?.sqlite) {
|
||||||
|
throw new Error("SQLite IPC bridge not available");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// Use IPC bridge with specific methods
|
||||||
// Create/Open database
|
this.sqlite = {
|
||||||
this.db = await this.sqlite.createConnection(
|
createConnection: async (options) => {
|
||||||
this.dbName,
|
await window.electron.ipcRenderer.invoke("sqlite-create-connection", {
|
||||||
false,
|
...options,
|
||||||
"no-encryption",
|
database: this.dbName,
|
||||||
1,
|
});
|
||||||
false,
|
},
|
||||||
);
|
query: async (options) => {
|
||||||
|
return await window.electron.ipcRenderer.invoke("sqlite-query", {
|
||||||
|
...options,
|
||||||
|
database: this.dbName,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
run: async (options) => {
|
||||||
|
return await window.electron.ipcRenderer.invoke("sqlite-run", {
|
||||||
|
...options,
|
||||||
|
database: this.dbName,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
execute: async (options) => {
|
||||||
|
await window.electron.ipcRenderer.invoke("sqlite-execute", {
|
||||||
|
...options,
|
||||||
|
database: this.dbName,
|
||||||
|
statements: [{ statement: options.statements }],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
} as SQLiteOperations;
|
||||||
|
|
||||||
await this.db.open();
|
// Create the connection (idempotent)
|
||||||
|
await this.sqlite!.createConnection({
|
||||||
|
database: this.dbName,
|
||||||
|
encrypted: false,
|
||||||
|
mode: "no-encryption",
|
||||||
|
});
|
||||||
|
|
||||||
// Set journal mode to WAL for better performance
|
// Optionally, test the connection
|
||||||
await this.db.execute("PRAGMA journal_mode=WAL;");
|
await this.sqlite!.query({
|
||||||
|
database: this.dbName,
|
||||||
|
statement: "SELECT 1",
|
||||||
|
});
|
||||||
|
|
||||||
// Run migrations
|
// Run migrations if needed
|
||||||
await this.runMigrations();
|
await this.runMigrations();
|
||||||
|
logger.info("[ElectronPlatformService] Database initialized successfully");
|
||||||
this.initialized = true;
|
|
||||||
logger.log("SQLite database initialized successfully");
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error initializing SQLite database:", error);
|
|
||||||
throw new Error("Failed to initialize database");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async runMigrations(): Promise<void> {
|
private async runMigrations(): Promise<void> {
|
||||||
if (!this.db) {
|
if (!this.sqlite) {
|
||||||
throw new Error("Database not initialized");
|
throw new Error("SQLite not initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create migrations table if it doesn't exist
|
// Create migrations table if it doesn't exist
|
||||||
await this.db.execute(`
|
await this.sqlite.execute({
|
||||||
CREATE TABLE IF NOT EXISTS migrations (
|
database: this.dbName,
|
||||||
|
statements: `CREATE TABLE IF NOT EXISTS migrations (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
name TEXT NOT NULL UNIQUE,
|
name TEXT NOT NULL UNIQUE,
|
||||||
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);`,
|
||||||
`);
|
});
|
||||||
|
|
||||||
// Get list of executed migrations
|
// Get list of executed migrations
|
||||||
const result = await this.db.query("SELECT name FROM migrations;");
|
const result = await this.sqlite.query({
|
||||||
|
database: this.dbName,
|
||||||
|
statement: "SELECT name FROM migrations;",
|
||||||
|
});
|
||||||
const executedMigrations = new Set(
|
const executedMigrations = new Set(
|
||||||
result.values?.map((row) => row[0]) || [],
|
(result.values as unknown[][])?.map(
|
||||||
|
(row: unknown[]) => row[0] as string,
|
||||||
|
) || [],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Run pending migrations in order
|
// Run pending migrations in order
|
||||||
const migrations: Migration[] = [
|
const migrations: Migration[] = [
|
||||||
{
|
{
|
||||||
@@ -174,13 +480,17 @@ export class ElectronPlatformService implements PlatformService {
|
|||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const migration of migrations) {
|
for (const migration of migrations) {
|
||||||
if (!executedMigrations.has(migration.name)) {
|
if (!executedMigrations.has(migration.name)) {
|
||||||
await this.db.execute(migration.sql);
|
await this.sqlite.execute({
|
||||||
await this.db.run("INSERT INTO migrations (name) VALUES (?)", [
|
database: this.dbName,
|
||||||
migration.name,
|
statements: migration.sql,
|
||||||
]);
|
});
|
||||||
|
await this.sqlite.run({
|
||||||
|
database: this.dbName,
|
||||||
|
statement: "INSERT INTO migrations (name) VALUES (?)",
|
||||||
|
values: [migration.name],
|
||||||
|
});
|
||||||
logger.log(`Migration ${migration.name} executed successfully`);
|
logger.log(`Migration ${migration.name} executed successfully`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -288,28 +598,194 @@ export class ElectronPlatformService implements PlatformService {
|
|||||||
throw new Error("Not implemented");
|
throw new Error("Not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private async enqueueOperation<T>(operation: () => Promise<T>): Promise<T> {
|
||||||
* @see PlatformService.dbQuery
|
// Wait for any existing operations to complete
|
||||||
*/
|
await this.operationQueue;
|
||||||
async dbQuery(sql: string, params?: unknown[]): Promise<QueryExecResult> {
|
|
||||||
await this.initializeDatabase();
|
// Create a new promise for this operation
|
||||||
if (!this.db) {
|
const operationPromise = (async () => {
|
||||||
throw new Error("Database not initialized");
|
try {
|
||||||
|
// Acquire lock
|
||||||
|
while (this.queueLock) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
|
}
|
||||||
|
this.queueLock = true;
|
||||||
|
|
||||||
|
// Execute operation
|
||||||
|
return await operation();
|
||||||
|
} finally {
|
||||||
|
// Release lock
|
||||||
|
this.queueLock = false;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Update the queue
|
||||||
|
this.operationQueue = operationPromise;
|
||||||
|
|
||||||
|
return operationPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getConnection(): Promise<void> {
|
||||||
|
// If we already have a connection promise, return it
|
||||||
|
if (this.connectionPromise) {
|
||||||
|
return this.connectionPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're already connected, return immediately
|
||||||
|
if (this.connectionState === "connected") {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new connection promise
|
||||||
|
this.connectionPromise = (async () => {
|
||||||
|
try {
|
||||||
|
this.connectionState = "connecting";
|
||||||
|
|
||||||
|
// Wait for any existing operations
|
||||||
|
await this.operationQueue;
|
||||||
|
|
||||||
|
// Create connection
|
||||||
|
await window.electron!.ipcRenderer.invoke("sqlite-create-connection", {
|
||||||
|
database: this.dbName,
|
||||||
|
encrypted: false,
|
||||||
|
mode: "no-encryption",
|
||||||
|
});
|
||||||
|
logger.debug("[ElectronPlatformService] Database connection created");
|
||||||
|
|
||||||
|
// Open database
|
||||||
|
await window.electron!.ipcRenderer.invoke("sqlite-open", {
|
||||||
|
database: this.dbName,
|
||||||
|
});
|
||||||
|
logger.debug("[ElectronPlatformService] Database opened");
|
||||||
|
|
||||||
|
// Verify database is open
|
||||||
|
const isOpen = await window.electron!.ipcRenderer.invoke(
|
||||||
|
"sqlite-is-db-open",
|
||||||
|
{
|
||||||
|
database: this.dbName,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (!isOpen) {
|
||||||
|
throw new Error("[ElectronPlatformService] Database failed to open");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.connectionState = "connected";
|
||||||
|
this.isConnectionOpen = true;
|
||||||
|
} catch (error) {
|
||||||
|
this.connectionState = "error";
|
||||||
|
this.connectionPromise = null;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return this.connectionPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async releaseConnection(): Promise<void> {
|
||||||
|
if (this.connectionState !== "connected") {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await this.db.query(sql, params || []);
|
// Close database
|
||||||
const values = result.values || [];
|
await window.electron!.ipcRenderer.invoke("sqlite-close", {
|
||||||
return {
|
database: this.dbName,
|
||||||
columns: [], // SQLite plugin doesn't provide column names in query result
|
});
|
||||||
values: values as SqlValue[][],
|
logger.debug("[ElectronPlatformService] Database closed");
|
||||||
};
|
|
||||||
|
// Close connection
|
||||||
|
await window.electron!.ipcRenderer.invoke("sqlite-close-connection", {
|
||||||
|
database: this.dbName,
|
||||||
|
});
|
||||||
|
logger.debug("[ElectronPlatformService] Database connection closed");
|
||||||
|
|
||||||
|
this.connectionState = "disconnected";
|
||||||
|
this.isConnectionOpen = false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error executing query:", error);
|
logger.error(
|
||||||
|
"[ElectronPlatformService] Failed to close database:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
this.connectionState = "error";
|
||||||
|
} finally {
|
||||||
|
this.connectionPromise = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a database query with proper connection lifecycle management.
|
||||||
|
* Opens connection, executes query, and ensures proper cleanup.
|
||||||
|
*
|
||||||
|
* @param sql - SQL query to execute
|
||||||
|
* @param params - Optional parameters for the query
|
||||||
|
* @returns Promise resolving to query results
|
||||||
|
* @throws Error if database operations fail
|
||||||
|
*/
|
||||||
|
async dbQuery<T = unknown>(
|
||||||
|
sql: string,
|
||||||
|
params: unknown[] = [],
|
||||||
|
): Promise<QueryExecResult<T>> {
|
||||||
|
if (this.dbFatalError) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Database query failed: ${error instanceof Error ? error.message : String(error)}`,
|
"Database is in a fatal error state. Please restart the app.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
logger.debug('[ElectronPlatformService] [dbQuery] Enqueuing operation', {
|
||||||
|
sql: sql.substring(0, 100) + (sql.length > 100 ? '...' : ''),
|
||||||
|
paramCount: params.length,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.enqueueOperation(async () => {
|
||||||
|
try {
|
||||||
|
// Get connection (will wait for existing connection if any)
|
||||||
|
console.log("[ElectronPlatformService] [dbQuery] Getting connection");
|
||||||
|
await this.getConnection();
|
||||||
|
console.log("[ElectronPlatformService] [dbQuery] Connection acquired");
|
||||||
|
// Execute query
|
||||||
|
console.log(
|
||||||
|
"[ElectronPlatformService] [dbQuery] Executing query",
|
||||||
|
{ sql, params },
|
||||||
|
);
|
||||||
|
const result = (await window.electron!.ipcRenderer.invoke(
|
||||||
|
"sqlite-query",
|
||||||
|
{
|
||||||
|
database: this.dbName,
|
||||||
|
statement: sql,
|
||||||
|
values: params,
|
||||||
|
},
|
||||||
|
)) as SQLiteQueryResult;
|
||||||
|
console.log(
|
||||||
|
"[ElectronPlatformService] [dbQuery] Query executed successfully",
|
||||||
|
{ result },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Process results
|
||||||
|
const columns = result.values?.[0] ? Object.keys(result.values[0]) : [];
|
||||||
|
const processedResult = {
|
||||||
|
columns,
|
||||||
|
values: (result.values || []).map(
|
||||||
|
(row: Record<string, unknown>) => row as T,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"[ElectronPlatformService] [dbQuery] Query processed successfully",
|
||||||
|
{ processedResult },
|
||||||
|
);
|
||||||
|
|
||||||
|
return processedResult;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"[ElectronPlatformService] [dbQuery] Query failed:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
// Release connection after query
|
||||||
|
await this.releaseConnection();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -319,23 +795,122 @@ export class ElectronPlatformService implements PlatformService {
|
|||||||
sql: string,
|
sql: string,
|
||||||
params?: unknown[],
|
params?: unknown[],
|
||||||
): Promise<{ changes: number; lastId?: number }> {
|
): Promise<{ changes: number; lastId?: number }> {
|
||||||
await this.initializeDatabase();
|
console.log("[ElectronPlatformService] [dbExec] Executing query", {
|
||||||
if (!this.db) {
|
sql,
|
||||||
throw new Error("Database not initialized");
|
params,
|
||||||
|
});
|
||||||
|
if (this.dbFatalError) {
|
||||||
|
throw new Error(
|
||||||
|
"Database is in a fatal error state. Please restart the app.",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return this.enqueueOperation(async () => {
|
||||||
|
try {
|
||||||
|
// Get connection (will wait for existing connection if any)
|
||||||
|
console.log("[ElectronPlatformService] [dbExec] Getting connection");
|
||||||
|
await this.getConnection();
|
||||||
|
console.log("[ElectronPlatformService] [dbExec] Connection acquired");
|
||||||
|
|
||||||
|
// Execute query
|
||||||
|
console.log(
|
||||||
|
"[ElectronPlatformService] [dbExec] Executing query",
|
||||||
|
{ sql, params },
|
||||||
|
);
|
||||||
|
const result = (await window.electron!.ipcRenderer.invoke(
|
||||||
|
"sqlite-run",
|
||||||
|
{
|
||||||
|
database: this.dbName,
|
||||||
|
statement: sql,
|
||||||
|
values: params,
|
||||||
|
},
|
||||||
|
)) as SQLiteQueryResult;
|
||||||
|
console.log(
|
||||||
|
"[ElectronPlatformService] [dbExec] Query executed successfully",
|
||||||
|
{ result },
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
changes: result.changes,
|
||||||
|
lastId: result.lastId,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
"[ElectronPlatformService] [dbExec] Query failed:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
// Release connection after query
|
||||||
|
await this.releaseConnection();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize(): Promise<void> {
|
||||||
|
await this.initializeDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(sql: string, params: unknown[] = []): Promise<void> {
|
||||||
|
await this.initializeDatabase();
|
||||||
|
if (this.dbFatalError) {
|
||||||
|
throw new Error(
|
||||||
|
"Database is in a fatal error state. Please restart the app.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!this.sqlite) {
|
||||||
|
throw new Error("SQLite not initialized");
|
||||||
|
}
|
||||||
|
await this.sqlite.run({
|
||||||
|
database: this.dbName,
|
||||||
|
statement: sql,
|
||||||
|
values: params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async close(): Promise<void> {
|
||||||
|
// Optionally implement close logic if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(sql: string, params: unknown[] = []): Promise<ElectronQueryExecResult> {
|
||||||
|
logger.debug('[ElectronPlatformService] [dbRun] Executing SQL:', {
|
||||||
|
sql: sql.substring(0, 100) + (sql.length > 100 ? '...' : ''),
|
||||||
|
params: formatLogObject(params),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await this.db.run(sql, params || []);
|
const result = (await window.electron!.ipcRenderer.invoke(
|
||||||
const changes = result.changes as Changes;
|
'sqlite-run',
|
||||||
|
{
|
||||||
|
database: this.dbName,
|
||||||
|
statement: sql,
|
||||||
|
values: params,
|
||||||
|
},
|
||||||
|
)) as SQLiteQueryResult;
|
||||||
|
|
||||||
|
logger.debug('[ElectronPlatformService] [dbRun] SQL execution result:', {
|
||||||
|
result: formatLogObject(result),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
changes: changes?.changes || 0,
|
success: true,
|
||||||
lastId: changes?.lastId,
|
changes: result.changes,
|
||||||
|
lastId: result.lastId,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error executing statement:", error);
|
logger.error('[ElectronPlatformService] [dbRun] SQL execution failed:', {
|
||||||
throw new Error(
|
error: error instanceof Error ? {
|
||||||
`Database execution failed: ${error instanceof Error ? error.message : String(error)}`,
|
name: error.name,
|
||||||
);
|
message: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
} : formatLogObject(error),
|
||||||
|
sql,
|
||||||
|
params: formatLogObject(params),
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
46
src/types/capacitor-sqlite-electron.d.ts
vendored
Normal file
46
src/types/capacitor-sqlite-electron.d.ts
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
declare module '@capacitor-community/sqlite/electron/dist/plugin.js' {
|
||||||
|
export class CapacitorSQLite {
|
||||||
|
constructor();
|
||||||
|
handle(event: Electron.IpcMainInvokeEvent, ...args: any[]): Promise<any>;
|
||||||
|
createConnection(options: any): Promise<any>;
|
||||||
|
closeConnection(options: any): Promise<any>;
|
||||||
|
echo(options: any): Promise<any>;
|
||||||
|
open(options: any): Promise<any>;
|
||||||
|
close(options: any): Promise<any>;
|
||||||
|
beginTransaction(options: any): Promise<any>;
|
||||||
|
commitTransaction(options: any): Promise<any>;
|
||||||
|
rollbackTransaction(options: any): Promise<any>;
|
||||||
|
isTransactionActive(options: any): Promise<any>;
|
||||||
|
getVersion(options: any): Promise<any>;
|
||||||
|
getTableList(options: any): Promise<any>;
|
||||||
|
execute(options: any): Promise<any>;
|
||||||
|
executeSet(options: any): Promise<any>;
|
||||||
|
run(options: any): Promise<any>;
|
||||||
|
query(options: any): Promise<any>;
|
||||||
|
isDBExists(options: any): Promise<any>;
|
||||||
|
isDBOpen(options: any): Promise<any>;
|
||||||
|
isDatabase(options: any): Promise<any>;
|
||||||
|
isTableExists(options: any): Promise<any>;
|
||||||
|
deleteDatabase(options: any): Promise<any>;
|
||||||
|
isJsonValid(options: any): Promise<any>;
|
||||||
|
importFromJson(options: any): Promise<any>;
|
||||||
|
exportToJson(options: any): Promise<any>;
|
||||||
|
createSyncTable(options: any): Promise<any>;
|
||||||
|
setSyncDate(options: any): Promise<any>;
|
||||||
|
getSyncDate(options: any): Promise<any>;
|
||||||
|
deleteExportedRows(options: any): Promise<any>;
|
||||||
|
addUpgradeStatement(options: any): Promise<any>;
|
||||||
|
copyFromAssets(options: any): Promise<any>;
|
||||||
|
getFromHTTPRequest(options: any): Promise<any>;
|
||||||
|
getDatabaseList(): Promise<any>;
|
||||||
|
checkConnectionsConsistency(options: any): Promise<any>;
|
||||||
|
isSecretStored(): Promise<any>;
|
||||||
|
isPassphraseValid(options: any): Promise<any>;
|
||||||
|
setEncryptionSecret(options: any): Promise<any>;
|
||||||
|
changeEncryptionSecret(options: any): Promise<any>;
|
||||||
|
clearEncryptionSecret(): Promise<any>;
|
||||||
|
isInConfigEncryption(): Promise<any>;
|
||||||
|
isDatabaseEncrypted(options: any): Promise<any>;
|
||||||
|
checkEncryptionSecret(options: any): Promise<any>;
|
||||||
|
}
|
||||||
|
}
|
||||||
87
src/types/electron.d.ts
vendored
Normal file
87
src/types/electron.d.ts
vendored
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
interface ElectronAPI {
|
||||||
|
sqlite: {
|
||||||
|
isAvailable: () => Promise<boolean>;
|
||||||
|
echo: (value: string) => Promise<{ value: string }>;
|
||||||
|
createConnection: (options: {
|
||||||
|
database: string;
|
||||||
|
version?: number;
|
||||||
|
readOnly?: boolean;
|
||||||
|
readonly?: boolean;
|
||||||
|
encryption?: string;
|
||||||
|
mode?: string;
|
||||||
|
useNative?: boolean;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}) => Promise<void>;
|
||||||
|
closeConnection: (options: { database: string }) => Promise<void>;
|
||||||
|
query: (options: { statement: string; values?: unknown[] }) => Promise<{
|
||||||
|
values?: Record<string, unknown>[];
|
||||||
|
changes?: { changes: number; lastId?: number };
|
||||||
|
}>;
|
||||||
|
run: (options: { statement: string; values?: unknown[] }) => Promise<{
|
||||||
|
changes?: { changes: number; lastId?: number };
|
||||||
|
}>;
|
||||||
|
execute: (options: { statements: { statement: string; values?: unknown[] }[] }) => Promise<{
|
||||||
|
changes?: { changes: number; lastId?: number }[];
|
||||||
|
}>;
|
||||||
|
getPlatform: () => Promise<string>;
|
||||||
|
};
|
||||||
|
ipcRenderer: {
|
||||||
|
on: (channel: string, func: (...args: unknown[]) => void) => void;
|
||||||
|
once: (channel: string, func: (...args: unknown[]) => void) => void;
|
||||||
|
send: (channel: string, data: unknown) => void;
|
||||||
|
invoke: (channel: string, ...args: unknown[]) => Promise<unknown>;
|
||||||
|
};
|
||||||
|
env: {
|
||||||
|
platform: string;
|
||||||
|
isDev: boolean;
|
||||||
|
};
|
||||||
|
getPath: (pathType: string) => Promise<string>;
|
||||||
|
getBasePath: () => Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
electron: {
|
||||||
|
ipcRenderer: {
|
||||||
|
on: (channel: string, func: (...args: unknown[]) => void) => void;
|
||||||
|
once: (channel: string, func: (...args: unknown[]) => void) => void;
|
||||||
|
send: (channel: string, data: unknown) => void;
|
||||||
|
invoke: (channel: string, ...args: unknown[]) => Promise<unknown>;
|
||||||
|
};
|
||||||
|
sqlite: {
|
||||||
|
isAvailable: () => Promise<boolean>;
|
||||||
|
echo: (value: string) => Promise<{ value: string }>;
|
||||||
|
createConnection: (options: {
|
||||||
|
database: string;
|
||||||
|
version?: number;
|
||||||
|
readOnly?: boolean;
|
||||||
|
readonly?: boolean;
|
||||||
|
encryption?: string;
|
||||||
|
mode?: string;
|
||||||
|
useNative?: boolean;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}) => Promise<void>;
|
||||||
|
closeConnection: (options: { database: string }) => Promise<void>;
|
||||||
|
query: (options: { statement: string; values?: unknown[] }) => Promise<{
|
||||||
|
values?: Record<string, unknown>[];
|
||||||
|
changes?: { changes: number; lastId?: number };
|
||||||
|
}>;
|
||||||
|
run: (options: { statement: string; values?: unknown[] }) => Promise<{
|
||||||
|
changes?: { changes: number; lastId?: number };
|
||||||
|
}>;
|
||||||
|
execute: (options: { statements: { statement: string; values?: unknown[] }[] }) => Promise<{
|
||||||
|
changes?: { changes: number; lastId?: number }[];
|
||||||
|
}>;
|
||||||
|
getPlatform: () => Promise<string>;
|
||||||
|
};
|
||||||
|
env: {
|
||||||
|
platform: string;
|
||||||
|
isDev: boolean;
|
||||||
|
};
|
||||||
|
getPath: (pathType: string) => Promise<string>;
|
||||||
|
getBasePath: () => Promise<string>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
32
src/types/global.d.ts
vendored
32
src/types/global.d.ts
vendored
@@ -1,4 +1,36 @@
|
|||||||
import type { QueryExecResult, SqlValue } from "./database";
|
import type { QueryExecResult, SqlValue } from "./database";
|
||||||
|
import type { CapacitorSQLite } from '@capacitor-community/sqlite';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
CapacitorSQLite: {
|
||||||
|
echo: (options: { value: string }) => Promise<{ value: string }>;
|
||||||
|
createConnection: (options: any) => Promise<any>;
|
||||||
|
closeConnection: (options: any) => Promise<any>;
|
||||||
|
execute: (options: any) => Promise<any>;
|
||||||
|
query: (options: any) => Promise<any>;
|
||||||
|
run: (options: any) => Promise<any>;
|
||||||
|
isAvailable: () => Promise<boolean>;
|
||||||
|
getPlatform: () => Promise<string>;
|
||||||
|
};
|
||||||
|
electron: {
|
||||||
|
sqlite: {
|
||||||
|
isAvailable: () => Promise<boolean>;
|
||||||
|
execute: (options: { method: string; database?: string; statement?: string; values?: unknown[]; statements?: string; encrypted?: boolean; mode?: string }) => Promise<unknown>;
|
||||||
|
};
|
||||||
|
// Add other electron IPC methods as needed
|
||||||
|
getPath: (pathType: string) => string;
|
||||||
|
send: (channel: string, data: any) => void;
|
||||||
|
receive: (channel: string, func: (...args: any[]) => void) => void;
|
||||||
|
env: {
|
||||||
|
isElectron: boolean;
|
||||||
|
isDev: boolean;
|
||||||
|
platform: string;
|
||||||
|
};
|
||||||
|
getBasePath: () => string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
declare module '@jlongster/sql.js' {
|
declare module '@jlongster/sql.js' {
|
||||||
interface SQL {
|
interface SQL {
|
||||||
|
|||||||
1
src/types/jeepq-sqlite.d.ts
vendored
Normal file
1
src/types/jeepq-sqlite.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
declare module '@jeepq/sqlite/loader';
|
||||||
169
src/utils/debug-electron.ts
Normal file
169
src/utils/debug-electron.ts
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
/**
|
||||||
|
* Debug utilities for Electron integration
|
||||||
|
* Helps verify the context bridge and SQLite functionality
|
||||||
|
*
|
||||||
|
* @author Matthew Raymer
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { logger } from "./logger";
|
||||||
|
|
||||||
|
// Define the SQLite interface
|
||||||
|
export interface SQLiteAPI {
|
||||||
|
isAvailable: () => Promise<boolean>;
|
||||||
|
echo: (value: string) => Promise<string>;
|
||||||
|
createConnection: (options: {
|
||||||
|
database: string;
|
||||||
|
version: number;
|
||||||
|
readOnly: boolean;
|
||||||
|
}) => Promise<void>;
|
||||||
|
closeConnection: (options: { database: string }) => Promise<void>;
|
||||||
|
query: (options: { statement: string }) => Promise<unknown>;
|
||||||
|
run: (options: { statement: string }) => Promise<unknown>;
|
||||||
|
execute: (options: {
|
||||||
|
statements: Array<{ statement: string }>;
|
||||||
|
}) => Promise<unknown>;
|
||||||
|
getPlatform: () => Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the IPC renderer interface
|
||||||
|
export interface IPCRenderer {
|
||||||
|
on: (channel: string, func: (...args: unknown[]) => void) => void;
|
||||||
|
once: (channel: string, func: (...args: unknown[]) => void) => void;
|
||||||
|
send: (channel: string, ...args: unknown[]) => void;
|
||||||
|
invoke: (channel: string, ...args: unknown[]) => Promise<unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the environment interface
|
||||||
|
export interface ElectronEnv {
|
||||||
|
platform: string;
|
||||||
|
isDev: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the complete electron interface
|
||||||
|
export interface ElectronAPI {
|
||||||
|
sqlite: SQLiteAPI;
|
||||||
|
ipcRenderer: IPCRenderer;
|
||||||
|
env: ElectronEnv;
|
||||||
|
getPath: (pathType: string) => Promise<string>;
|
||||||
|
getBasePath: () => Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the window.electron interface
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
electron: ElectronAPI;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function verifyElectronAPI(): Promise<void> {
|
||||||
|
logger.info("[Debug] Verifying Electron API exposure...");
|
||||||
|
|
||||||
|
// Check if window.electron exists
|
||||||
|
if (!window.electron) {
|
||||||
|
throw new Error("window.electron is not defined");
|
||||||
|
}
|
||||||
|
logger.info("[Debug] window.electron is available");
|
||||||
|
|
||||||
|
// Verify IPC renderer
|
||||||
|
if (!window.electron.ipcRenderer) {
|
||||||
|
throw new Error("IPC renderer is not available");
|
||||||
|
}
|
||||||
|
logger.info("[Debug] IPC renderer is available with methods:", {
|
||||||
|
hasOn: typeof window.electron.ipcRenderer.on === "function",
|
||||||
|
hasOnce: typeof window.electron.ipcRenderer.once === "function",
|
||||||
|
hasSend: typeof window.electron.ipcRenderer.send === "function",
|
||||||
|
hasInvoke: typeof window.electron.ipcRenderer.invoke === "function",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify SQLite API
|
||||||
|
if (!window.electron.sqlite) {
|
||||||
|
throw new Error("SQLite API is not available");
|
||||||
|
}
|
||||||
|
logger.info("[Debug] SQLite API is available with methods:", {
|
||||||
|
hasIsAvailable: typeof window.electron.sqlite.isAvailable === "function",
|
||||||
|
hasEcho: typeof window.electron.sqlite.echo === "function",
|
||||||
|
hasCreateConnection:
|
||||||
|
typeof window.electron.sqlite.createConnection === "function",
|
||||||
|
hasCloseConnection:
|
||||||
|
typeof window.electron.sqlite.closeConnection === "function",
|
||||||
|
hasQuery: typeof window.electron.sqlite.query === "function",
|
||||||
|
hasRun: typeof window.electron.sqlite.run === "function",
|
||||||
|
hasExecute: typeof window.electron.sqlite.execute === "function",
|
||||||
|
hasGetPlatform: typeof window.electron.sqlite.getPlatform === "function",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test SQLite availability
|
||||||
|
try {
|
||||||
|
const isAvailable = await window.electron.sqlite.isAvailable();
|
||||||
|
logger.info("[Debug] SQLite availability check:", { isAvailable });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("[Debug] SQLite availability check failed:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test echo functionality
|
||||||
|
try {
|
||||||
|
const echoResult = await window.electron.sqlite.echo("test");
|
||||||
|
logger.info("[Debug] SQLite echo test:", echoResult);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("[Debug] SQLite echo test failed:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify environment
|
||||||
|
logger.info("[Debug] Environment:", {
|
||||||
|
platform: window.electron.env.platform,
|
||||||
|
isDev: window.electron.env.isDev,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info("[Debug] Electron API verification complete");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export a function to test SQLite operations
|
||||||
|
export async function testSQLiteOperations(): Promise<void> {
|
||||||
|
logger.info("[Debug] Testing SQLite operations...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Test connection creation
|
||||||
|
logger.info("[Debug] Creating test connection...");
|
||||||
|
await window.electron.sqlite.createConnection({
|
||||||
|
database: "test",
|
||||||
|
version: 1,
|
||||||
|
readOnly: false,
|
||||||
|
});
|
||||||
|
logger.info("[Debug] Test connection created successfully");
|
||||||
|
|
||||||
|
// Test query
|
||||||
|
logger.info("[Debug] Testing query operation...");
|
||||||
|
const queryResult = await window.electron.sqlite.query({
|
||||||
|
statement: "SELECT 1 as test",
|
||||||
|
});
|
||||||
|
logger.info("[Debug] Query test result:", queryResult);
|
||||||
|
|
||||||
|
// Test run
|
||||||
|
logger.info("[Debug] Testing run operation...");
|
||||||
|
const runResult = await window.electron.sqlite.run({
|
||||||
|
statement:
|
||||||
|
"CREATE TABLE IF NOT EXISTS test_table (id INTEGER PRIMARY KEY)",
|
||||||
|
});
|
||||||
|
logger.info("[Debug] Run test result:", runResult);
|
||||||
|
|
||||||
|
// Test execute
|
||||||
|
logger.info("[Debug] Testing execute operation...");
|
||||||
|
const executeResult = await window.electron.sqlite.execute({
|
||||||
|
statements: [
|
||||||
|
{ statement: "INSERT INTO test_table (id) VALUES (1)" },
|
||||||
|
{ statement: "SELECT * FROM test_table" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
logger.info("[Debug] Execute test result:", executeResult);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
logger.info("[Debug] Closing test connection...");
|
||||||
|
await window.electron.sqlite.closeConnection({ database: "test" });
|
||||||
|
logger.info("[Debug] Test connection closed");
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("[Debug] SQLite operation test failed:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info("[Debug] SQLite operations test complete");
|
||||||
|
}
|
||||||
@@ -68,9 +68,9 @@
|
|||||||
class="inline-block text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
class="inline-block text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
||||||
@click="
|
@click="
|
||||||
() =>
|
() =>
|
||||||
($refs.userNameDialog as UserNameDialog).open(
|
($refs.userNameDialog as UserNameDialog).open((name) => {
|
||||||
(name) => (givenName = name),
|
if (name) givenName = name;
|
||||||
)
|
})
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
Set Your Name
|
Set Your Name
|
||||||
@@ -437,7 +437,7 @@
|
|||||||
<b>{{ endorserLimits?.maxClaimsPerWeek || "?" }}</b> for this week.
|
<b>{{ endorserLimits?.maxClaimsPerWeek || "?" }}</b> for this week.
|
||||||
Your claims counter resets at
|
Your claims counter resets at
|
||||||
<b class="whitespace-nowrap">{{
|
<b class="whitespace-nowrap">{{
|
||||||
readableDate(endorserLimits?.nextWeekBeginDateTime)
|
readableDate(endorserLimits?.nextWeekBeginDateTime ?? "")
|
||||||
}}</b>
|
}}</b>
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-3 text-sm">
|
<p class="mt-3 text-sm">
|
||||||
@@ -454,7 +454,7 @@
|
|||||||
<i>(You cannot register anyone on your first day.)</i>
|
<i>(You cannot register anyone on your first day.)</i>
|
||||||
Your registration counter resets at
|
Your registration counter resets at
|
||||||
<b class="whitespace-nowrap">
|
<b class="whitespace-nowrap">
|
||||||
{{ readableDate(endorserLimits?.nextMonthBeginDateTime) }}
|
{{ readableDate(endorserLimits?.nextMonthBeginDateTime ?? "") }}
|
||||||
</b>
|
</b>
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-3 text-sm">
|
<p class="mt-3 text-sm">
|
||||||
@@ -463,7 +463,7 @@
|
|||||||
<b>{{ imageLimits?.maxImagesPerWeek || "?" }}</b> for this week. Your
|
<b>{{ imageLimits?.maxImagesPerWeek || "?" }}</b> for this week. Your
|
||||||
image counter resets at
|
image counter resets at
|
||||||
<b class="whitespace-nowrap">{{
|
<b class="whitespace-nowrap">{{
|
||||||
readableDate(imageLimits?.nextWeekBeginDateTime)
|
readableDate(imageLimits?.nextWeekBeginDateTime ?? "")
|
||||||
}}</b>
|
}}</b>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -976,11 +976,10 @@ import Dexie from "dexie";
|
|||||||
import "dexie-export-import";
|
import "dexie-export-import";
|
||||||
// @ts-expect-error - they aren't exporting it but it's there
|
// @ts-expect-error - they aren't exporting it but it's there
|
||||||
import { ImportProgress } from "dexie-export-import";
|
import { ImportProgress } from "dexie-export-import";
|
||||||
import { LeafletMouseEvent } from "leaflet";
|
|
||||||
import * as R from "ramda";
|
import * as R from "ramda";
|
||||||
import { IIdentifier } from "@veramo/core";
|
import { IIdentifier } from "@veramo/core";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue, Hook } from "vue-facing-decorator";
|
||||||
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { useClipboard } from "@vueuse/core";
|
||||||
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
|
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
|
||||||
@@ -1016,15 +1015,14 @@ import {
|
|||||||
import * as databaseUtil from "../db/databaseUtil";
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
import {
|
import {
|
||||||
clearPasskeyToken,
|
clearPasskeyToken,
|
||||||
EndorserRateLimits,
|
|
||||||
ErrorResponse,
|
|
||||||
errorStringForLog,
|
errorStringForLog,
|
||||||
fetchEndorserRateLimits,
|
fetchEndorserRateLimits,
|
||||||
fetchImageRateLimits,
|
fetchImageRateLimits,
|
||||||
getHeaders,
|
getHeaders,
|
||||||
ImageRateLimits,
|
|
||||||
tokenExpiryTimeDescription,
|
tokenExpiryTimeDescription,
|
||||||
} from "../libs/endorserServer";
|
} from "../libs/endorserServer";
|
||||||
|
import { EndorserRateLimits, ImageRateLimits } from "../interfaces/limits";
|
||||||
|
import { ErrorResponse } from "../interfaces/common";
|
||||||
import {
|
import {
|
||||||
DAILY_CHECK_TITLE,
|
DAILY_CHECK_TITLE,
|
||||||
DIRECT_PUSH_TITLE,
|
DIRECT_PUSH_TITLE,
|
||||||
@@ -1040,7 +1038,6 @@ const inputImportFileNameRef = ref<Blob>();
|
|||||||
components: {
|
components: {
|
||||||
EntityIcon,
|
EntityIcon,
|
||||||
ImageMethodDialog,
|
ImageMethodDialog,
|
||||||
LeafletMouseEvent,
|
|
||||||
LMap,
|
LMap,
|
||||||
LMarker,
|
LMarker,
|
||||||
LTileLayer,
|
LTileLayer,
|
||||||
@@ -1117,8 +1114,11 @@ export default class AccountViewView extends Vue {
|
|||||||
*
|
*
|
||||||
* @throws Will display specific messages to the user based on different errors.
|
* @throws Will display specific messages to the user based on different errors.
|
||||||
*/
|
*/
|
||||||
|
@Hook("mounted")
|
||||||
async mounted() {
|
async mounted() {
|
||||||
try {
|
try {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log("[AccountViewView] -- mounted", process.env.VITE_PLATFORM);
|
||||||
// Initialize component state with values from the database or defaults
|
// Initialize component state with values from the database or defaults
|
||||||
await this.initializeState();
|
await this.initializeState();
|
||||||
await this.processIdentity();
|
await this.processIdentity();
|
||||||
@@ -1145,7 +1145,12 @@ export default class AccountViewView extends Vue {
|
|||||||
throw Error("Unable to load profile.");
|
throw Error("Unable to load profile.");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.status === 404) {
|
if (
|
||||||
|
error &&
|
||||||
|
typeof error === "object" &&
|
||||||
|
"status" in error &&
|
||||||
|
error.status === 404
|
||||||
|
) {
|
||||||
// this is ok: the profile is not yet created
|
// this is ok: the profile is not yet created
|
||||||
} else {
|
} else {
|
||||||
databaseUtil.logConsoleAndDb(
|
databaseUtil.logConsoleAndDb(
|
||||||
@@ -1232,7 +1237,9 @@ export default class AccountViewView extends Vue {
|
|||||||
* Initializes component state with values from the database or defaults.
|
* Initializes component state with values from the database or defaults.
|
||||||
*/
|
*/
|
||||||
async initializeState() {
|
async initializeState() {
|
||||||
|
console.log("[AccountViewView] initializeState");
|
||||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||||
|
console.log("[AccountViewView] initializeState", { settings });
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
await db.open();
|
await db.open();
|
||||||
settings = await retrieveSettingsForActiveAccount();
|
settings = await retrieveSettingsForActiveAccount();
|
||||||
@@ -1676,7 +1683,9 @@ export default class AccountViewView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async uploadImportFile(event: Event) {
|
async uploadImportFile(event: Event) {
|
||||||
inputImportFileNameRef.value = (event.target as EventTarget).files[0];
|
inputImportFileNameRef.value = (
|
||||||
|
event.target as HTMLInputElement
|
||||||
|
).files?.[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
showContactImport() {
|
showContactImport() {
|
||||||
@@ -1886,16 +1895,77 @@ export default class AccountViewView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async onClickSaveApiServer() {
|
async onClickSaveApiServer() {
|
||||||
await databaseUtil.updateDefaultSettings({
|
console.log("[AccountViewView] -- Starting API server update", {
|
||||||
apiServer: this.apiServerInput,
|
current: this.apiServer,
|
||||||
|
new: this.apiServerInput,
|
||||||
|
component: "AccountViewView",
|
||||||
});
|
});
|
||||||
if (USE_DEXIE_DB) {
|
logger.debug("[AccountViewView] Starting API server update", {
|
||||||
await db.open();
|
current: this.apiServer,
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
new: this.apiServerInput,
|
||||||
|
component: "AccountViewView",
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.debug("[AccountViewView] Calling updateDefaultSettings");
|
||||||
|
await databaseUtil.updateDefaultSettings({
|
||||||
apiServer: this.apiServerInput,
|
apiServer: this.apiServerInput,
|
||||||
});
|
});
|
||||||
|
logger.debug("[AccountViewView] updateDefaultSettings completed");
|
||||||
|
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
logger.debug("[AccountViewView] Updating Dexie DB");
|
||||||
|
await db.open();
|
||||||
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
apiServer: this.apiServerInput,
|
||||||
|
});
|
||||||
|
logger.debug("[AccountViewView] Dexie DB update completed");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.apiServer = this.apiServerInput;
|
||||||
|
logger.debug("[AccountViewView] Local state updated", {
|
||||||
|
apiServer: this.apiServer,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify the update
|
||||||
|
const settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||||
|
logger.debug("[AccountViewView] Settings verification", {
|
||||||
|
retrieved: settings.apiServer,
|
||||||
|
expected: this.apiServerInput,
|
||||||
|
match: settings.apiServer === this.apiServerInput,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show success notification
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "success",
|
||||||
|
title: "API Server Updated",
|
||||||
|
text: "API server settings saved successfully.",
|
||||||
|
},
|
||||||
|
3000,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("[AccountViewView] API server update failed", {
|
||||||
|
error,
|
||||||
|
current: this.apiServer,
|
||||||
|
attempted: this.apiServerInput,
|
||||||
|
});
|
||||||
|
console.log("[AccountViewView] API server update failed", {
|
||||||
|
error,
|
||||||
|
current: this.apiServer,
|
||||||
|
attempted: this.apiServerInput,
|
||||||
|
});
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error Saving API Server",
|
||||||
|
text: "Failed to save API server settings. Please try again.",
|
||||||
|
},
|
||||||
|
5000,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this.apiServer = this.apiServerInput;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClickSavePartnerServer() {
|
async onClickSavePartnerServer() {
|
||||||
@@ -2072,6 +2142,7 @@ export default class AccountViewView extends Vue {
|
|||||||
const headers = await getHeaders(this.activeDid);
|
const headers = await getHeaders(this.activeDid);
|
||||||
const payload: UserProfile = {
|
const payload: UserProfile = {
|
||||||
description: this.userProfileDesc,
|
description: this.userProfileDesc,
|
||||||
|
issuerDid: this.activeDid,
|
||||||
};
|
};
|
||||||
if (this.userProfileLatitude && this.userProfileLongitude) {
|
if (this.userProfileLatitude && this.userProfileLongitude) {
|
||||||
payload.locLat = this.userProfileLatitude;
|
payload.locLat = this.userProfileLatitude;
|
||||||
@@ -2113,11 +2184,14 @@ export default class AccountViewView extends Vue {
|
|||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
logConsoleAndDb("Error saving profile: " + errorStringForLog(error));
|
logConsoleAndDb("Error saving profile: " + errorStringForLog(error));
|
||||||
}
|
}
|
||||||
const errorMessage: string =
|
let errorMessage = "There was an error saving your profile.";
|
||||||
error.response?.data?.error?.message ||
|
if (error instanceof AxiosError) {
|
||||||
error.response?.data?.error ||
|
errorMessage =
|
||||||
error.message ||
|
error.response?.data?.error?.message ||
|
||||||
"There was an error saving your profile.";
|
error.response?.data?.error ||
|
||||||
|
error.message ||
|
||||||
|
errorMessage;
|
||||||
|
}
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -2208,11 +2282,14 @@ export default class AccountViewView extends Vue {
|
|||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
logConsoleAndDb("Error deleting profile: " + errorStringForLog(error));
|
logConsoleAndDb("Error deleting profile: " + errorStringForLog(error));
|
||||||
}
|
}
|
||||||
const errorMessage: string =
|
let errorMessage = "There was an error deleting your profile.";
|
||||||
error.response?.data?.error?.message ||
|
if (error instanceof AxiosError) {
|
||||||
error.response?.data?.error ||
|
errorMessage =
|
||||||
error.message ||
|
error.response?.data?.error?.message ||
|
||||||
"There was an error deleting your profile.";
|
error.response?.data?.error ||
|
||||||
|
error.message ||
|
||||||
|
errorMessage;
|
||||||
|
}
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
|
|||||||
@@ -548,11 +548,7 @@ import { db } from "../db/index";
|
|||||||
import { logConsoleAndDb } from "../db/databaseUtil";
|
import { logConsoleAndDb } from "../db/databaseUtil";
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from "../db/tables/contacts";
|
||||||
import * as serverUtil from "../libs/endorserServer";
|
import * as serverUtil from "../libs/endorserServer";
|
||||||
import {
|
import { GenericCredWrapper, OfferClaim, ProviderInfo } from "../interfaces";
|
||||||
GenericCredWrapper,
|
|
||||||
OfferVerifiableCredential,
|
|
||||||
ProviderInfo,
|
|
||||||
} from "../interfaces";
|
|
||||||
import * as libsUtil from "../libs/util";
|
import * as libsUtil from "../libs/util";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||||
@@ -978,7 +974,7 @@ export default class ClaimView extends Vue {
|
|||||||
openFulfillGiftDialog() {
|
openFulfillGiftDialog() {
|
||||||
const giver: libsUtil.GiverReceiverInputInfo = {
|
const giver: libsUtil.GiverReceiverInputInfo = {
|
||||||
did: libsUtil.offerGiverDid(
|
did: libsUtil.offerGiverDid(
|
||||||
this.veriClaim as GenericCredWrapper<OfferVerifiableCredential>,
|
this.veriClaim as GenericCredWrapper<OfferClaim>,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
(this.$refs.customGiveDialog as GiftedDialog).open(
|
(this.$refs.customGiveDialog as GiftedDialog).open(
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ import * as databaseUtil from "../db/databaseUtil";
|
|||||||
import {
|
import {
|
||||||
AgreeVerifiableCredential,
|
AgreeVerifiableCredential,
|
||||||
GiveSummaryRecord,
|
GiveSummaryRecord,
|
||||||
GiveVerifiableCredential,
|
GiveActionClaim,
|
||||||
} from "../interfaces";
|
} from "../interfaces";
|
||||||
import {
|
import {
|
||||||
createEndorserJwtVcFromClaim,
|
createEndorserJwtVcFromClaim,
|
||||||
@@ -276,7 +276,7 @@ export default class ContactAmountssView extends Vue {
|
|||||||
// Make claim
|
// Make claim
|
||||||
// I use clone here because otherwise it gets a Proxy object.
|
// I use clone here because otherwise it gets a Proxy object.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const origClaim: GiveVerifiableCredential = R.clone(record.fullClaim);
|
const origClaim: GiveActionClaim = R.clone(record.fullClaim);
|
||||||
if (record.fullClaim["@context"] == SCHEMA_ORG_CONTEXT) {
|
if (record.fullClaim["@context"] == SCHEMA_ORG_CONTEXT) {
|
||||||
delete origClaim["@context"];
|
delete origClaim["@context"];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -526,10 +526,6 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
const contact = {
|
const contact = {
|
||||||
did: contactInfo.did,
|
did: contactInfo.did,
|
||||||
name: contactInfo.name || "",
|
name: contactInfo.name || "",
|
||||||
email: contactInfo.email || "",
|
|
||||||
phone: contactInfo.phone || "",
|
|
||||||
company: contactInfo.company || "",
|
|
||||||
title: contactInfo.title || "",
|
|
||||||
notes: contactInfo.notes || "",
|
notes: contactInfo.notes || "",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -846,11 +842,9 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
text: "Do you want to register them?",
|
text: "Do you want to register them?",
|
||||||
onCancel: async (stopAsking?: boolean) => {
|
onCancel: async (stopAsking?: boolean) => {
|
||||||
if (stopAsking) {
|
if (stopAsking) {
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
await databaseUtil.updateAccountSettings(this.activeDid, {
|
||||||
await platformService.dbExec(
|
hideRegisterPromptOnNewContact: stopAsking,
|
||||||
"UPDATE settings SET hideRegisterPromptOnNewContact = ? WHERE key = ?",
|
});
|
||||||
[stopAsking, MASTER_SETTINGS_KEY],
|
|
||||||
);
|
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
hideRegisterPromptOnNewContact: stopAsking,
|
hideRegisterPromptOnNewContact: stopAsking,
|
||||||
@@ -861,11 +855,9 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
},
|
},
|
||||||
onNo: async (stopAsking?: boolean) => {
|
onNo: async (stopAsking?: boolean) => {
|
||||||
if (stopAsking) {
|
if (stopAsking) {
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
await databaseUtil.updateAccountSettings(this.activeDid, {
|
||||||
await platformService.dbExec(
|
hideRegisterPromptOnNewContact: stopAsking,
|
||||||
"UPDATE settings SET hideRegisterPromptOnNewContact = ? WHERE key = ?",
|
});
|
||||||
[stopAsking, MASTER_SETTINGS_KEY],
|
|
||||||
);
|
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
hideRegisterPromptOnNewContact: stopAsking,
|
hideRegisterPromptOnNewContact: stopAsking,
|
||||||
|
|||||||
@@ -240,8 +240,8 @@ import * as databaseUtil from "../db/databaseUtil";
|
|||||||
import {
|
import {
|
||||||
GenericCredWrapper,
|
GenericCredWrapper,
|
||||||
GenericVerifiableCredential,
|
GenericVerifiableCredential,
|
||||||
GiveVerifiableCredential,
|
GiveActionClaim,
|
||||||
OfferVerifiableCredential,
|
OfferClaim,
|
||||||
} from "../interfaces";
|
} from "../interfaces";
|
||||||
import {
|
import {
|
||||||
capitalizeAndInsertSpacesBeforeCaps,
|
capitalizeAndInsertSpacesBeforeCaps,
|
||||||
@@ -657,7 +657,7 @@ export default class DIDView extends Vue {
|
|||||||
*/
|
*/
|
||||||
public claimAmount(claim: GenericVerifiableCredential) {
|
public claimAmount(claim: GenericVerifiableCredential) {
|
||||||
if (claim.claimType === "GiveAction") {
|
if (claim.claimType === "GiveAction") {
|
||||||
const giveClaim = claim.claim as GiveVerifiableCredential;
|
const giveClaim = claim.claim as GiveActionClaim;
|
||||||
if (giveClaim.object?.unitCode && giveClaim.object?.amountOfThisGood) {
|
if (giveClaim.object?.unitCode && giveClaim.object?.amountOfThisGood) {
|
||||||
return displayAmount(
|
return displayAmount(
|
||||||
giveClaim.object.unitCode,
|
giveClaim.object.unitCode,
|
||||||
@@ -667,7 +667,7 @@ export default class DIDView extends Vue {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
} else if (claim.claimType === "Offer") {
|
} else if (claim.claimType === "Offer") {
|
||||||
const offerClaim = claim.claim as OfferVerifiableCredential;
|
const offerClaim = claim.claim as OfferClaim;
|
||||||
if (
|
if (
|
||||||
offerClaim.includesObject?.unitCode &&
|
offerClaim.includesObject?.unitCode &&
|
||||||
offerClaim.includesObject?.amountOfThisGood
|
offerClaim.includesObject?.amountOfThisGood
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ import {
|
|||||||
} from "../constants/app";
|
} from "../constants/app";
|
||||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||||
import * as databaseUtil from "../db/databaseUtil";
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
import { GenericCredWrapper, GiveVerifiableCredential } from "../interfaces";
|
import { GenericCredWrapper, GiveActionClaim } from "../interfaces";
|
||||||
import {
|
import {
|
||||||
createAndSubmitGive,
|
createAndSubmitGive,
|
||||||
didInfo,
|
didInfo,
|
||||||
@@ -311,7 +311,7 @@ export default class GiftedDetails extends Vue {
|
|||||||
imageUrl = "";
|
imageUrl = "";
|
||||||
message = "";
|
message = "";
|
||||||
offerId = "";
|
offerId = "";
|
||||||
prevCredToEdit?: GenericCredWrapper<GiveVerifiableCredential>;
|
prevCredToEdit?: GenericCredWrapper<GiveActionClaim>;
|
||||||
providerProjectId = "";
|
providerProjectId = "";
|
||||||
providerProjectName = "a project";
|
providerProjectName = "a project";
|
||||||
providedByProject = false; // basically static, based on input; if we allow changing then let's fix things (see below)
|
providedByProject = false; // basically static, based on input; if we allow changing then let's fix things (see below)
|
||||||
@@ -328,7 +328,7 @@ export default class GiftedDetails extends Vue {
|
|||||||
this.prevCredToEdit = (this.$route.query["prevCredToEdit"] as string)
|
this.prevCredToEdit = (this.$route.query["prevCredToEdit"] as string)
|
||||||
? (JSON.parse(
|
? (JSON.parse(
|
||||||
this.$route.query["prevCredToEdit"] as string,
|
this.$route.query["prevCredToEdit"] as string,
|
||||||
) as GenericCredWrapper<GiveVerifiableCredential>)
|
) as GenericCredWrapper<GiveActionClaim>)
|
||||||
: undefined;
|
: undefined;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
@@ -883,7 +883,7 @@ export default class GiftedDetails extends Vue {
|
|||||||
? this.fulfillsProjectId
|
? this.fulfillsProjectId
|
||||||
: undefined;
|
: undefined;
|
||||||
const giveClaim = hydrateGive(
|
const giveClaim = hydrateGive(
|
||||||
this.prevCredToEdit?.claim as GiveVerifiableCredential,
|
this.prevCredToEdit?.claim as GiveActionClaim,
|
||||||
giverDid,
|
giverDid,
|
||||||
recipientDid,
|
recipientDid,
|
||||||
this.description,
|
this.description,
|
||||||
|
|||||||
@@ -350,7 +350,7 @@ import {
|
|||||||
import { GiveSummaryRecord } from "../interfaces";
|
import { GiveSummaryRecord } from "../interfaces";
|
||||||
import * as serverUtil from "../libs/endorserServer";
|
import * as serverUtil from "../libs/endorserServer";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { GiveRecordWithContactInfo } from "../types";
|
import { GiveRecordWithContactInfo } from "../interfaces/give";
|
||||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||||
|
|
||||||
interface Claim {
|
interface Claim {
|
||||||
@@ -610,7 +610,10 @@ export default class HomeView extends Vue {
|
|||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
logConsoleAndDb("Error retrieving settings or feed: " + err, true);
|
logConsoleAndDb(
|
||||||
|
"[initializeIdentity] Error retrieving settings or feed: " + err,
|
||||||
|
true,
|
||||||
|
);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -1687,7 +1690,7 @@ export default class HomeView extends Vue {
|
|||||||
* @param event Event object
|
* @param event Event object
|
||||||
* @param imageUrl URL of image to cache
|
* @param imageUrl URL of image to cache
|
||||||
*/
|
*/
|
||||||
async cacheImageData(event: Event, imageUrl: string) {
|
async cacheImageData(_event: Event, imageUrl: string) {
|
||||||
try {
|
try {
|
||||||
// For images that might fail CORS, just store the URL
|
// For images that might fail CORS, just store the URL
|
||||||
// The Web Share API will handle sharing the URL appropriately
|
// The Web Share API will handle sharing the URL appropriately
|
||||||
@@ -1748,7 +1751,7 @@ export default class HomeView extends Vue {
|
|||||||
this.axios,
|
this.axios,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.type === "success") {
|
if (result.success) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
|||||||
import * as databaseUtil from "../db/databaseUtil";
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
import { retrieveAllAccountsMetadata } from "../libs/util";
|
import { retrieveAllAccountsMetadata } from "../libs/util";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||||
|
|
||||||
@Component({ components: { QuickNav } })
|
@Component({ components: { QuickNav } })
|
||||||
export default class IdentitySwitcherView extends Vue {
|
export default class IdentitySwitcherView extends Vue {
|
||||||
@@ -167,10 +168,13 @@ export default class IdentitySwitcherView extends Vue {
|
|||||||
if (did === "0") {
|
if (did === "0") {
|
||||||
did = undefined;
|
did = undefined;
|
||||||
}
|
}
|
||||||
await db.open();
|
await databaseUtil.updateDefaultSettings({ activeDid: did });
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
if (USE_DEXIE_DB) {
|
||||||
activeDid: did,
|
await db.open();
|
||||||
});
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
activeDid: did,
|
||||||
|
});
|
||||||
|
}
|
||||||
this.$router.push({ name: "account" });
|
this.$router.push({ name: "account" });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,9 +186,15 @@ export default class IdentitySwitcherView extends Vue {
|
|||||||
title: "Delete Identity?",
|
title: "Delete Identity?",
|
||||||
text: "Are you sure you want to erase this identity? (There is no undo. You may want to select it and back it up just in case.)",
|
text: "Are you sure you want to erase this identity? (There is no undo. You may want to select it and back it up just in case.)",
|
||||||
onYes: async () => {
|
onYes: async () => {
|
||||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
const accountsDB = await accountsDBPromise;
|
await platformService.dbExec(`DELETE FROM accounts WHERE id = ?`, [
|
||||||
await accountsDB.accounts.delete(id);
|
id,
|
||||||
|
]);
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||||
|
const accountsDB = await accountsDBPromise;
|
||||||
|
await accountsDB.accounts.delete(id);
|
||||||
|
}
|
||||||
this.otherIdentities = this.otherIdentities.filter(
|
this.otherIdentities = this.otherIdentities.filter(
|
||||||
(ident) => ident.id !== id,
|
(ident) => ident.id !== id,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -324,7 +324,7 @@ export default class InviteOneView extends Vue {
|
|||||||
);
|
);
|
||||||
await axios.post(
|
await axios.post(
|
||||||
this.apiServer + "/api/userUtil/invite",
|
this.apiServer + "/api/userUtil/invite",
|
||||||
{ inviteJwt: inviteJwt, notes: notes },
|
{ inviteIdentifier, inviteJwt, notes, expiresAt },
|
||||||
{ headers },
|
{ headers },
|
||||||
);
|
);
|
||||||
const newInvite = {
|
const newInvite = {
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ import QuickNav from "../components/QuickNav.vue";
|
|||||||
import TopMessage from "../components/TopMessage.vue";
|
import TopMessage from "../components/TopMessage.vue";
|
||||||
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||||
import { GenericCredWrapper, OfferVerifiableCredential } from "../interfaces";
|
import { GenericCredWrapper, OfferClaim } from "../interfaces";
|
||||||
import {
|
import {
|
||||||
createAndSubmitOffer,
|
createAndSubmitOffer,
|
||||||
didInfo,
|
didInfo,
|
||||||
@@ -268,7 +268,7 @@ export default class OfferDetailsView extends Vue {
|
|||||||
/** Offer ID for editing */
|
/** Offer ID for editing */
|
||||||
offerId = "";
|
offerId = "";
|
||||||
/** Previous offer data for editing */
|
/** Previous offer data for editing */
|
||||||
prevCredToEdit?: GenericCredWrapper<OfferVerifiableCredential>;
|
prevCredToEdit?: GenericCredWrapper<OfferClaim>;
|
||||||
/** Project ID if offer is for project */
|
/** Project ID if offer is for project */
|
||||||
projectId = "";
|
projectId = "";
|
||||||
/** Project name display */
|
/** Project name display */
|
||||||
@@ -330,7 +330,7 @@ export default class OfferDetailsView extends Vue {
|
|||||||
this.prevCredToEdit = (this.$route.query["prevCredToEdit"] as string)
|
this.prevCredToEdit = (this.$route.query["prevCredToEdit"] as string)
|
||||||
? (JSON.parse(
|
? (JSON.parse(
|
||||||
this.$route.query["prevCredToEdit"] as string,
|
this.$route.query["prevCredToEdit"] as string,
|
||||||
) as GenericCredWrapper<OfferVerifiableCredential>)
|
) as GenericCredWrapper<OfferClaim>)
|
||||||
: undefined;
|
: undefined;
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
@@ -703,7 +703,7 @@ export default class OfferDetailsView extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.type === "error" || this.isCreationError(result.response)) {
|
if (!result.success) {
|
||||||
const errorMessage = this.getCreationErrorMessage(result);
|
const errorMessage = this.getCreationErrorMessage(result);
|
||||||
logger.error("Error with offer creation result:", result);
|
logger.error("Error with offer creation result:", result);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
@@ -768,7 +768,7 @@ export default class OfferDetailsView extends Vue {
|
|||||||
: undefined;
|
: undefined;
|
||||||
const projectId = this.offeredToProject ? this.projectId : undefined;
|
const projectId = this.offeredToProject ? this.projectId : undefined;
|
||||||
const offerClaim = hydrateOffer(
|
const offerClaim = hydrateOffer(
|
||||||
this.prevCredToEdit?.claim as OfferVerifiableCredential,
|
this.prevCredToEdit?.claim as OfferClaim,
|
||||||
this.activeDid,
|
this.activeDid,
|
||||||
recipientDid,
|
recipientDid,
|
||||||
this.descriptionOfItem,
|
this.descriptionOfItem,
|
||||||
|
|||||||
@@ -613,9 +613,9 @@ import {
|
|||||||
GenericVerifiableCredential,
|
GenericVerifiableCredential,
|
||||||
GenericCredWrapper,
|
GenericCredWrapper,
|
||||||
GiveSummaryRecord,
|
GiveSummaryRecord,
|
||||||
GiveVerifiableCredential,
|
GiveActionClaim,
|
||||||
OfferSummaryRecord,
|
OfferSummaryRecord,
|
||||||
OfferVerifiableCredential,
|
OfferClaim,
|
||||||
PlanSummaryRecord,
|
PlanSummaryRecord,
|
||||||
} from "../interfaces";
|
} from "../interfaces";
|
||||||
import GiftedDialog from "../components/GiftedDialog.vue";
|
import GiftedDialog from "../components/GiftedDialog.vue";
|
||||||
@@ -1269,7 +1269,7 @@ export default class ProjectViewView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
checkIsFulfillable(offer: OfferSummaryRecord) {
|
checkIsFulfillable(offer: OfferSummaryRecord) {
|
||||||
const offerRecord: GenericCredWrapper<OfferVerifiableCredential> = {
|
const offerRecord: GenericCredWrapper<OfferClaim> = {
|
||||||
...serverUtil.BLANK_GENERIC_SERVER_RECORD,
|
...serverUtil.BLANK_GENERIC_SERVER_RECORD,
|
||||||
claim: offer.fullClaim,
|
claim: offer.fullClaim,
|
||||||
claimType: "Offer",
|
claimType: "Offer",
|
||||||
@@ -1279,13 +1279,13 @@ export default class ProjectViewView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onClickFulfillGiveToOffer(offer: OfferSummaryRecord) {
|
onClickFulfillGiveToOffer(offer: OfferSummaryRecord) {
|
||||||
const offerRecord: GenericCredWrapper<OfferVerifiableCredential> = {
|
const offerClaimCred: GenericCredWrapper<OfferClaim> = {
|
||||||
...serverUtil.BLANK_GENERIC_SERVER_RECORD,
|
...serverUtil.BLANK_GENERIC_SERVER_RECORD,
|
||||||
claim: offer.fullClaim,
|
claim: offer.fullClaim,
|
||||||
issuer: offer.offeredByDid,
|
issuer: offer.offeredByDid,
|
||||||
};
|
};
|
||||||
const giver: libsUtil.GiverReceiverInputInfo = {
|
const giver: libsUtil.GiverReceiverInputInfo = {
|
||||||
did: libsUtil.offerGiverDid(offerRecord),
|
did: libsUtil.offerGiverDid(offerClaimCred),
|
||||||
};
|
};
|
||||||
(this.$refs.giveDialogToThis as GiftedDialog).open(
|
(this.$refs.giveDialogToThis as GiftedDialog).open(
|
||||||
giver,
|
giver,
|
||||||
@@ -1327,7 +1327,7 @@ export default class ProjectViewView extends Vue {
|
|||||||
* @param confirmerIdList optional list of DIDs who confirmed; if missing, doesn't do a full server check
|
* @param confirmerIdList optional list of DIDs who confirmed; if missing, doesn't do a full server check
|
||||||
*/
|
*/
|
||||||
checkIsConfirmable(give: GiveSummaryRecord, confirmerIdList?: string[]) {
|
checkIsConfirmable(give: GiveSummaryRecord, confirmerIdList?: string[]) {
|
||||||
const giveDetails: GenericCredWrapper<GiveVerifiableCredential> = {
|
const giveDetails: GenericCredWrapper<GiveActionClaim> = {
|
||||||
...serverUtil.BLANK_GENERIC_SERVER_RECORD,
|
...serverUtil.BLANK_GENERIC_SERVER_RECORD,
|
||||||
claim: give.fullClaim,
|
claim: give.fullClaim,
|
||||||
claimType: "GiveAction",
|
claimType: "GiveAction",
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ import TopMessage from "../components/TopMessage.vue";
|
|||||||
import { NotificationIface, APP_SERVER, USE_DEXIE_DB } from "../constants/app";
|
import { NotificationIface, APP_SERVER, USE_DEXIE_DB } from "../constants/app";
|
||||||
import * as databaseUtil from "../db/databaseUtil";
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||||
import { retrieveAccountMetadata } from "../libs/util";
|
import { retrieveFullyDecryptedAccount } from "../libs/util";
|
||||||
import { generateEndorserJwtUrlForAccount } from "../libs/endorserServer";
|
import { generateEndorserJwtUrlForAccount } from "../libs/endorserServer";
|
||||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
@@ -75,7 +75,7 @@ export default class ShareMyContactInfoView extends Vue {
|
|||||||
const isRegistered = !!settings.isRegistered;
|
const isRegistered = !!settings.isRegistered;
|
||||||
const profileImageUrl = settings.profileImageUrl || "";
|
const profileImageUrl = settings.profileImageUrl || "";
|
||||||
|
|
||||||
const account = await retrieveAccountMetadata(activeDid);
|
const account = await retrieveFullyDecryptedAccount(activeDid);
|
||||||
|
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
const contactQueryResult = await platformService.dbQuery(
|
const contactQueryResult = await platformService.dbQuery(
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ test('New offers for another user', async ({ page }) => {
|
|||||||
// as user 1, go to the home page and check that two offers are shown as new
|
// as user 1, go to the home page and check that two offers are shown as new
|
||||||
await switchToUser(page, user01Did);
|
await switchToUser(page, user01Did);
|
||||||
await page.goto('./');
|
await page.goto('./');
|
||||||
// await page.getByTestId('closeOnboardingAndFinish').click();
|
|
||||||
let offerNumElem = page.getByTestId('newDirectOffersActivityNumber');
|
let offerNumElem = page.getByTestId('newDirectOffersActivityNumber');
|
||||||
await expect(offerNumElem).toHaveText('2');
|
await expect(offerNumElem).toHaveText('2');
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"target": "ES2020",
|
"target": "ES2020",
|
||||||
"outDir": "dist-electron",
|
"outDir": "dist-electron",
|
||||||
"rootDir": "src",
|
"rootDir": ".",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"types": ["vite/client"],
|
"types": ["vite/client", "electron"],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
},
|
},
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
"src/**/*.ts",
|
"src/**/*.ts",
|
||||||
"src/**/*.d.ts",
|
"src/**/*.d.ts",
|
||||||
"src/**/*.tsx",
|
"src/**/*.tsx",
|
||||||
"src/**/*.vue"
|
"src/**/*.vue",
|
||||||
|
"electron/src/**/*.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
42
vite.config.app.electron.mts
Normal file
42
vite.config.app.electron.mts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* vite.config.app.electron.mts
|
||||||
|
*
|
||||||
|
* Vite configuration for building the web application for Electron.
|
||||||
|
* This config outputs to 'dist/' (like the web build), sets VITE_PLATFORM to 'electron',
|
||||||
|
* and disables PWA plugins and web-only features. Use this when you want to package
|
||||||
|
* the web app for Electron but keep the output structure identical to the web build.
|
||||||
|
*
|
||||||
|
* Author: Matthew Raymer
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { defineConfig, mergeConfig } from 'vite';
|
||||||
|
import { createBuildConfig } from './vite.config.common.mts';
|
||||||
|
import { loadAppConfig } from './vite.config.utils.mts';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export default defineConfig(async () => {
|
||||||
|
// Set mode to 'electron' for platform-specific config
|
||||||
|
const mode = 'electron';
|
||||||
|
const baseConfig = await createBuildConfig(mode);
|
||||||
|
const appConfig = await loadAppConfig();
|
||||||
|
|
||||||
|
// Override build output directory to 'dist/'
|
||||||
|
const buildConfig = {
|
||||||
|
outDir: path.resolve(__dirname, 'dist'),
|
||||||
|
emptyOutDir: true,
|
||||||
|
rollupOptions: {
|
||||||
|
input: path.resolve(__dirname, 'index.html'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// No PWA plugins or web-only plugins for Electron
|
||||||
|
return mergeConfig(baseConfig, {
|
||||||
|
build: buildConfig,
|
||||||
|
plugins: [],
|
||||||
|
define: {
|
||||||
|
'process.env.VITE_PLATFORM': JSON.stringify('electron'),
|
||||||
|
'process.env.VITE_PWA_ENABLED': JSON.stringify(false),
|
||||||
|
'process.env.VITE_DISABLE_PWA': JSON.stringify(true),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,104 +1,102 @@
|
|||||||
import { defineConfig, mergeConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import { createBuildConfig } from "./vite.config.common.mts";
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
export default defineConfig(async () => {
|
export default defineConfig({
|
||||||
const baseConfig = await createBuildConfig('electron');
|
build: {
|
||||||
|
outDir: 'dist-electron',
|
||||||
return mergeConfig(baseConfig, {
|
rollupOptions: {
|
||||||
build: {
|
input: {
|
||||||
outDir: 'dist-electron',
|
main: path.resolve(__dirname, 'src/electron/main.ts'),
|
||||||
rollupOptions: {
|
preload: path.resolve(__dirname, 'electron/src/preload.ts')
|
||||||
input: {
|
|
||||||
main: path.resolve(__dirname, 'src/electron/main.ts'),
|
|
||||||
preload: path.resolve(__dirname, 'src/electron/preload.js'),
|
|
||||||
},
|
|
||||||
external: ['electron'],
|
|
||||||
output: {
|
|
||||||
format: 'cjs',
|
|
||||||
entryFileNames: '[name].js',
|
|
||||||
assetFileNames: 'assets/[name].[ext]',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
target: 'node18',
|
external: [
|
||||||
minify: false,
|
// Node.js built-ins
|
||||||
sourcemap: true,
|
'stream',
|
||||||
},
|
'path',
|
||||||
resolve: {
|
'fs',
|
||||||
alias: {
|
'crypto',
|
||||||
'@': path.resolve(__dirname, 'src'),
|
'buffer',
|
||||||
|
'util',
|
||||||
|
'events',
|
||||||
|
'url',
|
||||||
|
'assert',
|
||||||
|
'os',
|
||||||
|
'net',
|
||||||
|
'http',
|
||||||
|
'https',
|
||||||
|
'zlib',
|
||||||
|
'child_process',
|
||||||
|
// Electron and Capacitor
|
||||||
|
'electron',
|
||||||
|
'@capacitor/core',
|
||||||
|
'@capacitor-community/sqlite',
|
||||||
|
'@capacitor-community/sqlite/electron',
|
||||||
|
'@capacitor-community/sqlite/electron/dist/plugin',
|
||||||
|
'better-sqlite3-multiple-ciphers',
|
||||||
|
// HTTP clients
|
||||||
|
'axios',
|
||||||
|
'axios/dist/axios',
|
||||||
|
'axios/dist/node/axios.cjs'
|
||||||
|
],
|
||||||
|
output: {
|
||||||
|
format: 'es',
|
||||||
|
entryFileNames: '[name].mjs',
|
||||||
|
assetFileNames: 'assets/[name].[ext]',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
optimizeDeps: {
|
target: 'node18',
|
||||||
include: ['@/utils/logger']
|
minify: false,
|
||||||
|
sourcemap: true,
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, 'src'),
|
||||||
|
// Use Node.js version of axios in electron
|
||||||
|
'axios': 'axios/dist/node/axios.cjs'
|
||||||
},
|
},
|
||||||
plugins: [
|
},
|
||||||
{
|
optimizeDeps: {
|
||||||
name: 'typescript-transform',
|
exclude: [
|
||||||
transform(code: string, id: string) {
|
'stream',
|
||||||
if (id.endsWith('main.ts')) {
|
'path',
|
||||||
// Replace the logger import with inline logger
|
'fs',
|
||||||
return code.replace(
|
'crypto',
|
||||||
/import\s*{\s*logger\s*}\s*from\s*['"]@\/utils\/logger['"];?/,
|
'buffer',
|
||||||
`const logger = {
|
'util',
|
||||||
log: (...args) => console.log(...args),
|
'events',
|
||||||
error: (...args) => console.error(...args),
|
'url',
|
||||||
info: (...args) => console.info(...args),
|
'assert',
|
||||||
warn: (...args) => console.warn(...args),
|
'os',
|
||||||
debug: (...args) => console.debug(...args),
|
'net',
|
||||||
};`
|
'http',
|
||||||
);
|
'https',
|
||||||
}
|
'zlib',
|
||||||
return code;
|
'child_process',
|
||||||
}
|
'axios',
|
||||||
},
|
'axios/dist/axios',
|
||||||
{
|
'axios/dist/node/axios.cjs'
|
||||||
name: 'remove-sw-imports',
|
]
|
||||||
transform(code: string, id: string) {
|
},
|
||||||
// Remove service worker imports and registrations
|
plugins: [
|
||||||
if (id.includes('registerServiceWorker') ||
|
{
|
||||||
id.includes('register-service-worker') ||
|
name: 'typescript-transform',
|
||||||
id.includes('sw_scripts') ||
|
transform(code: string, id: string) {
|
||||||
id.includes('PushNotificationPermission') ||
|
if (id.endsWith('main.ts')) {
|
||||||
code.includes('navigator.serviceWorker')) {
|
return code.replace(
|
||||||
return {
|
/import\s*{\s*logger\s*}\s*from\s*['"]@\/utils\/logger['"];?/,
|
||||||
code: code
|
`const logger = {
|
||||||
.replace(/import.*registerServiceWorker.*$/mg, '')
|
log: (...args) => console.log(...args),
|
||||||
.replace(/import.*register-service-worker.*$/mg, '')
|
error: (...args) => console.error(...args),
|
||||||
.replace(/navigator\.serviceWorker/g, 'undefined')
|
info: (...args) => console.info(...args),
|
||||||
.replace(/if\s*\([^)]*serviceWorker[^)]*\)\s*{[^}]*}/g, '')
|
warn: (...args) => console.warn(...args),
|
||||||
.replace(/import.*workbox.*$/mg, '')
|
debug: (...args) => console.debug(...args),
|
||||||
.replace(/importScripts\([^)]*\)/g, '')
|
};`
|
||||||
};
|
);
|
||||||
}
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'remove-sw-files',
|
|
||||||
enforce: 'pre',
|
|
||||||
resolveId(id: string) {
|
|
||||||
// Prevent service worker files from being included
|
|
||||||
if (id.includes('sw.js') ||
|
|
||||||
id.includes('workbox') ||
|
|
||||||
id.includes('registerSW.js') ||
|
|
||||||
id.includes('manifest.webmanifest')) {
|
|
||||||
return '\0empty';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
load(id: string) {
|
|
||||||
if (id === '\0empty') {
|
|
||||||
return 'export default {}';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
return code;
|
||||||
}
|
}
|
||||||
],
|
}
|
||||||
ssr: {
|
],
|
||||||
noExternal: ['@/utils/logger']
|
base: './',
|
||||||
},
|
publicDir: 'public',
|
||||||
base: './',
|
|
||||||
publicDir: 'public',
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
21
vite.config.renderer.mts
Normal file
21
vite.config.renderer.mts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import vue from '@vitejs/plugin-vue';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
root: path.resolve(__dirname, '.'),
|
||||||
|
base: './',
|
||||||
|
build: {
|
||||||
|
outDir: path.resolve(__dirname, 'dist-electron/www'),
|
||||||
|
emptyOutDir: false,
|
||||||
|
rollupOptions: {
|
||||||
|
input: path.resolve(__dirname, 'dist/www/index.html'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, 'src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user