Compare commits
122 Commits
home-icon-
...
contacts-v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3544d7278d | ||
|
|
d3110506ea | ||
| 8609f8458d | |||
| 8f5c34bc5f | |||
| b0d61b95ea | |||
| af7bd236a3 | |||
| d719338bcc | |||
| 6ddf2d1012 | |||
|
|
1b2d4b623a | ||
|
|
16d5c917d2 | ||
| 5976a4995e | |||
| dcd0cc4c20 | |||
| b3ca6c9d91 | |||
| e9d800f601 | |||
| b939a5e592 | |||
| aa62037fae | |||
| 722020ea86 | |||
| 96aa3f4a54 | |||
| c0c5f9842b | |||
| be27ca1855 | |||
| 92e4570672 | |||
| 820ae727ed | |||
| dbeb1c6b4b | |||
| 573e4b206a | |||
| 2ea7479d75 | |||
| 9ac9713172 | |||
| 41dad3254d | |||
| 485eac59a0 | |||
|
|
73fc32b75d | ||
|
|
3d8e40e92b | ||
| 38e67f3533 | |||
| 7f63ee7c80 | |||
| 6a47f0d3e7 | |||
| fc50a9d4c6 | |||
|
|
45f43ff363 | ||
|
|
7b1d4c4849 | ||
|
|
c1f2c3951a | ||
| 9d4f726c31 | |||
| 1d7f626645 | |||
| c5228ba7ec | |||
| 6e1fcd8dee | |||
| 5bb563d694 | |||
| a3951c9d66 | |||
| aa177a9b8c | |||
| 03cb4720b8 | |||
|
|
0e65431f43 | ||
| 297c5a2dbb | |||
| ef3bfcdbd2 | |||
| ec1f27bab1 | |||
| 01c33069c4 | |||
| c637d39dc9 | |||
| 3e90bafbd1 | |||
|
|
d2c3e5db05 | ||
|
|
e824fcce2e | ||
|
|
f2c49872a6 | ||
|
|
229d9184b2 | ||
|
|
29908b77e3 | ||
|
|
16cad04e5c | ||
|
|
e4f859a116 | ||
|
|
7f17a3d9c7 | ||
|
|
2d4d9691ca | ||
|
|
63575b36ed | ||
|
|
2eb46367bc | ||
|
|
cea0456148 | ||
|
|
6f5db13a49 | ||
|
|
068662625d | ||
|
|
23627835f9 | ||
|
|
f1ba6f9231 | ||
|
|
137fce3e30 | ||
|
|
7166dadbc0 | ||
|
|
bc274bdf7f | ||
|
|
082f8c0126 | ||
|
|
fd09c7e426 | ||
|
|
be40643379 | ||
|
|
835a270e65 | ||
|
|
13682a1930 | ||
|
|
669a66c24c | ||
|
|
13505b539e | ||
|
|
07ac340733 | ||
|
|
ba2b2fc543 | ||
| 21184e7625 | |||
| 8d1511e38f | |||
|
|
b18112b869 | ||
|
|
a228a9b1c0 | ||
|
|
1560ff0829 | ||
| 7de4125eb7 | |||
|
|
81d4f0c762 | ||
| 4c1b4fe651 | |||
|
|
e63541ef53 | ||
| 0bfc18c385 | |||
|
|
35f5df6b6b | ||
|
|
0f1ac2b230 | ||
| 3c0bdeaed3 | |||
| 11f2527b04 | |||
| 5d8175aeeb | |||
| b6b95cb0d0 | |||
| 655c5188a4 | |||
| 8b7451330f | |||
| b8fbc3f7a6 | |||
| 92dadba1cb | |||
| 3a6f585de0 | |||
| 2647c5a77d | |||
|
|
682fceb1c6 | ||
|
|
e0013008b4 | ||
| 0674d98670 | |||
|
|
ee441d1aea | ||
|
|
75f6e99200 | ||
|
|
52c9e57ef4 | ||
| 603823d808 | |||
| 5f24f4975d | |||
| 5057d7d07f | |||
| 946e88d903 | |||
|
|
cbfb1ebf57 | ||
| a38934e38d | |||
| a3bdcfd168 | |||
| 83771caee1 | |||
| da35b225cd | |||
| 8c3920e108 | |||
| 54f269054f | |||
|
|
574520d9b3 | ||
| 6556eb55a3 | |||
| 634e2bb2fb |
153
.cursor/rules/absurd-sql.mdc
Normal file
153
.cursor/rules/absurd-sql.mdc
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
# Absurd SQL - Cursor Development Guide
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
Absurd SQL is a backend implementation for sql.js that enables persistent SQLite databases in the browser by using IndexedDB as a block storage system. This guide provides rules and best practices for developing with this project in Cursor.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
```
|
||||||
|
absurd-sql/
|
||||||
|
├── src/ # Source code
|
||||||
|
├── dist/ # Built files
|
||||||
|
├── package.json # Dependencies and scripts
|
||||||
|
├── rollup.config.js # Build configuration
|
||||||
|
└── jest.config.js # Test configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Rules
|
||||||
|
|
||||||
|
### 1. Worker Thread Requirements
|
||||||
|
- All SQL operations MUST be performed in a worker thread
|
||||||
|
- Main thread should only handle worker initialization and communication
|
||||||
|
- Never block the main thread with database operations
|
||||||
|
|
||||||
|
### 2. Code Organization
|
||||||
|
- Keep worker code in separate files (e.g., `*.worker.js`)
|
||||||
|
- Use ES modules for imports/exports
|
||||||
|
- Follow the project's existing module structure
|
||||||
|
|
||||||
|
### 3. Required Headers
|
||||||
|
When developing locally or deploying, ensure these headers are set:
|
||||||
|
```
|
||||||
|
Cross-Origin-Opener-Policy: same-origin
|
||||||
|
Cross-Origin-Embedder-Policy: require-corp
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Browser Compatibility
|
||||||
|
- Primary target: Modern browsers with SharedArrayBuffer support
|
||||||
|
- Fallback mode: Safari (with limitations)
|
||||||
|
- Always test in both modes
|
||||||
|
|
||||||
|
### 5. Database Configuration
|
||||||
|
Recommended database settings:
|
||||||
|
```sql
|
||||||
|
PRAGMA journal_mode=MEMORY;
|
||||||
|
PRAGMA page_size=8192; -- Optional, but recommended
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Development Workflow
|
||||||
|
1. Install dependencies:
|
||||||
|
```bash
|
||||||
|
yarn add @jlongster/sql.js absurd-sql
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Development commands:
|
||||||
|
- `yarn build` - Build the project
|
||||||
|
- `yarn jest` - Run tests
|
||||||
|
- `yarn serve` - Start development server
|
||||||
|
|
||||||
|
### 7. Testing Guidelines
|
||||||
|
- Write tests for both SharedArrayBuffer and fallback modes
|
||||||
|
- Use Jest for testing
|
||||||
|
- Include performance benchmarks for critical operations
|
||||||
|
|
||||||
|
### 8. Performance Considerations
|
||||||
|
- Use bulk operations when possible
|
||||||
|
- Monitor read/write performance
|
||||||
|
- Consider using transactions for multiple operations
|
||||||
|
- Avoid unnecessary database connections
|
||||||
|
|
||||||
|
### 9. Error Handling
|
||||||
|
- Implement proper error handling for:
|
||||||
|
- Worker initialization failures
|
||||||
|
- Database connection issues
|
||||||
|
- Concurrent access conflicts (in fallback mode)
|
||||||
|
- Storage quota exceeded scenarios
|
||||||
|
|
||||||
|
### 10. Security Best Practices
|
||||||
|
- Never expose database operations directly to the client
|
||||||
|
- Validate all SQL queries
|
||||||
|
- Implement proper access controls
|
||||||
|
- Handle sensitive data appropriately
|
||||||
|
|
||||||
|
### 11. Code Style
|
||||||
|
- Follow ESLint configuration
|
||||||
|
- Use async/await for asynchronous operations
|
||||||
|
- Document complex database operations
|
||||||
|
- Include comments for non-obvious optimizations
|
||||||
|
|
||||||
|
### 12. Debugging
|
||||||
|
- Use `jest-debug` for debugging tests
|
||||||
|
- Monitor IndexedDB usage in browser dev tools
|
||||||
|
- Check worker communication in console
|
||||||
|
- Use performance monitoring tools
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Worker Initialization
|
||||||
|
```javascript
|
||||||
|
// Main thread
|
||||||
|
import { initBackend } from 'absurd-sql/dist/indexeddb-main-thread';
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
let worker = new Worker(new URL('./index.worker.js', import.meta.url));
|
||||||
|
initBackend(worker);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Setup
|
||||||
|
```javascript
|
||||||
|
// Worker thread
|
||||||
|
import initSqlJs from '@jlongster/sql.js';
|
||||||
|
import { SQLiteFS } from 'absurd-sql';
|
||||||
|
import IndexedDBBackend from 'absurd-sql/dist/indexeddb-backend';
|
||||||
|
|
||||||
|
async function setupDatabase() {
|
||||||
|
let SQL = await initSqlJs({ locateFile: file => file });
|
||||||
|
let sqlFS = new SQLiteFS(SQL.FS, new IndexedDBBackend());
|
||||||
|
SQL.register_for_idb(sqlFS);
|
||||||
|
|
||||||
|
SQL.FS.mkdir('/sql');
|
||||||
|
SQL.FS.mount(sqlFS, {}, '/sql');
|
||||||
|
|
||||||
|
return new SQL.Database('/sql/db.sqlite', { filename: true });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
1. SharedArrayBuffer not available
|
||||||
|
- Check COOP/COEP headers
|
||||||
|
- Verify browser support
|
||||||
|
- Test fallback mode
|
||||||
|
|
||||||
|
2. Worker initialization failures
|
||||||
|
- Check file paths
|
||||||
|
- Verify module imports
|
||||||
|
- Check browser console for errors
|
||||||
|
|
||||||
|
3. Performance issues
|
||||||
|
- Monitor IndexedDB usage
|
||||||
|
- Check for unnecessary operations
|
||||||
|
- Verify transaction usage
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
- [Project Demo](https://priceless-keller-d097e5.netlify.app/)
|
||||||
|
- [Example Project](https://github.com/jlongster/absurd-example-project)
|
||||||
|
- [Blog Post](https://jlongster.com/future-sql-web)
|
||||||
|
- [SQL.js Documentation](https://github.com/sql-js/sql.js/)
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
description:
|
description:
|
||||||
globs:
|
globs:
|
||||||
alwaysApply: true
|
alwaysApply: false
|
||||||
---
|
---
|
||||||
# Camera Implementation Documentation
|
# Camera Implementation Documentation
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
# iOS doesn't like spaces in the app title.
|
# iOS doesn't like spaces in the app title.
|
||||||
TIME_SAFARI_APP_TITLE="TimeSafari_Dev"
|
TIME_SAFARI_APP_TITLE="TimeSafari_Dev"
|
||||||
VITE_APP_SERVER=http://localhost:3000
|
VITE_APP_SERVER=http://localhost:8080
|
||||||
# This is the claim ID for actions in the BVC project, with the JWT ID on this environment (not production).
|
# This is the claim ID for actions in the BVC project, with the JWT ID on this environment (not production).
|
||||||
VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F
|
VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F
|
||||||
VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000
|
VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000
|
||||||
# Using shared server by default to ease setup, which works for shared test users.
|
# Using shared server by default to ease setup, which works for shared test users.
|
||||||
VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app
|
VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app
|
||||||
VITE_DEFAULT_PARTNER_API_SERVER=http://localhost:3000
|
VITE_DEFAULT_PARTNER_API_SERVER=http://localhost:3000
|
||||||
|
#VITE_DEFAULT_PUSH_SERVER... can't be set up with localhost domain
|
||||||
VITE_PASSKEYS_ENABLED=true
|
VITE_PASSKEYS_ENABLED=true
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
# Admin DID credentials
|
|
||||||
ADMIN_DID=did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F
|
|
||||||
ADMIN_PRIVATE_KEY=2b6472c026ec2aa2c4235c994a63868fc9212d18b58f6cbfe861b52e71330f5b
|
|
||||||
|
|
||||||
# API Configuration
|
|
||||||
ENDORSER_API_URL=https://test-api.endorser.ch/api/v2/claim
|
|
||||||
@@ -9,3 +9,4 @@ VITE_DEFAULT_ENDORSER_API_SERVER=https://api.endorser.ch
|
|||||||
|
|
||||||
VITE_DEFAULT_IMAGE_API_SERVER=https://image-api.timesafari.app
|
VITE_DEFAULT_IMAGE_API_SERVER=https://image-api.timesafari.app
|
||||||
VITE_DEFAULT_PARTNER_API_SERVER=https://partner-api.endorser.ch
|
VITE_DEFAULT_PARTNER_API_SERVER=https://partner-api.endorser.ch
|
||||||
|
VITE_DEFAULT_PUSH_SERVER=https://timesafari.app
|
||||||
|
|||||||
@@ -9,4 +9,5 @@ VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch
|
|||||||
|
|
||||||
VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app
|
VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app
|
||||||
VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch
|
VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch
|
||||||
|
VITE_DEFAULT_PUSH_SERVER=https://test.timesafari.app
|
||||||
VITE_PASSKEYS_ENABLED=true
|
VITE_PASSKEYS_ENABLED=true
|
||||||
|
|||||||
@@ -4,6 +4,12 @@ module.exports = {
|
|||||||
node: true,
|
node: true,
|
||||||
es2022: true,
|
es2022: true,
|
||||||
},
|
},
|
||||||
|
ignorePatterns: [
|
||||||
|
'node_modules/',
|
||||||
|
'dist/',
|
||||||
|
'dist-electron/',
|
||||||
|
'*.d.ts'
|
||||||
|
],
|
||||||
extends: [
|
extends: [
|
||||||
"plugin:vue/vue3-recommended",
|
"plugin:vue/vue3-recommended",
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
|
|||||||
71
BUILDING.md
71
BUILDING.md
@@ -9,19 +9,6 @@ For a quick dev environment setup, use [pkgx](https://pkgx.dev).
|
|||||||
- Node.js (LTS version recommended)
|
- Node.js (LTS version recommended)
|
||||||
- npm (comes with Node.js)
|
- npm (comes with Node.js)
|
||||||
- Git
|
- Git
|
||||||
- For Android builds: Android Studio with SDK installed
|
|
||||||
- For iOS builds: macOS with Xcode and ruby gems & bundle
|
|
||||||
- `pkgx +rubygems.org sh`
|
|
||||||
|
|
||||||
- ... and you may have to fix these, especially with pkgx
|
|
||||||
|
|
||||||
```bash
|
|
||||||
gem_path=$(which gem)
|
|
||||||
shortened_path="${gem_path:h:h}"
|
|
||||||
export GEM_HOME=$shortened_path
|
|
||||||
export GEM_PATH=$shortened_path
|
|
||||||
```
|
|
||||||
|
|
||||||
- For desktop builds: Additional build tools based on your OS
|
- For desktop builds: Additional build tools based on your OS
|
||||||
|
|
||||||
## Forks
|
## Forks
|
||||||
@@ -84,7 +71,7 @@ Install dependencies:
|
|||||||
* For test, build the app (because test server is not yet set up to build):
|
* For test, build the app (because test server is not yet set up to build):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_PASSKEYS_ENABLED=true npm run build
|
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_DEFAULT_PUSH_SERVER=https://test.timesafari.app VITE_PASSKEYS_ENABLED=true npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
... and transfer to the test server:
|
... and transfer to the test server:
|
||||||
@@ -241,7 +228,9 @@ docker run -d \
|
|||||||
1. Build the electron app in production mode:
|
1. Build the electron app in production mode:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run build:electron-prod
|
npm run build:web
|
||||||
|
npm run build:electron
|
||||||
|
npm run electron:build-mac
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Package the Electron app for macOS:
|
2. Package the Electron app for macOS:
|
||||||
@@ -324,6 +313,32 @@ npm run build:electron-prod && npm run electron:start
|
|||||||
|
|
||||||
Prerequisites: macOS with Xcode installed
|
Prerequisites: macOS with Xcode installed
|
||||||
|
|
||||||
|
#### First-time iOS Configuration
|
||||||
|
|
||||||
|
- Generate certificates inside XCode.
|
||||||
|
|
||||||
|
- Right-click on App and under Signing & Capabilities set the Team.
|
||||||
|
|
||||||
|
#### Each Release
|
||||||
|
|
||||||
|
0. First time (or if XCode dependencies change):
|
||||||
|
|
||||||
|
- `pkgx +rubygems.org sh`
|
||||||
|
|
||||||
|
- ... and you may have to fix these, especially with pkgx
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gem_path=$(which gem)
|
||||||
|
shortened_path="${gem_path:h:h}"
|
||||||
|
export GEM_HOME=$shortened_path
|
||||||
|
export GEM_PATH=$shortened_path
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ios/App
|
||||||
|
pod install
|
||||||
|
```
|
||||||
|
|
||||||
1. Build the web assets:
|
1. Build the web assets:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -332,6 +347,7 @@ Prerequisites: macOS with Xcode installed
|
|||||||
npm run build:capacitor
|
npm run build:capacitor
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
2. Update iOS project with latest build:
|
2. Update iOS project with latest build:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -355,10 +371,10 @@ Prerequisites: macOS with Xcode installed
|
|||||||
|
|
||||||
```
|
```
|
||||||
cd ios/App
|
cd ios/App
|
||||||
xcrun agvtool new-version 15
|
xcrun agvtool new-version 25
|
||||||
# Unfortunately this edits Info.plist directly.
|
# Unfortunately this edits Info.plist directly.
|
||||||
#xcrun agvtool new-marketing-version 0.4.5
|
#xcrun agvtool new-marketing-version 0.4.5
|
||||||
cat App.xcodeproj/project.pbxproj | sed "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 0.4.5;/g" > temp
|
cat App.xcodeproj/project.pbxproj | sed "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 0.5.0;/g" > temp
|
||||||
mv temp App.xcodeproj/project.pbxproj
|
mv temp App.xcodeproj/project.pbxproj
|
||||||
cd -
|
cd -
|
||||||
```
|
```
|
||||||
@@ -371,28 +387,25 @@ Prerequisites: macOS with Xcode installed
|
|||||||
|
|
||||||
6. Use Xcode to build and run on simulator or device.
|
6. Use Xcode to build and run on simulator or device.
|
||||||
|
|
||||||
|
* Select Product -> Destination with some Simulator version. Then click the run arrow.
|
||||||
|
|
||||||
7. Release
|
7. Release
|
||||||
|
|
||||||
* Under "General" renamed a bunch of things to "Time Safari"
|
* Someday: Under "General" we want to rename a bunch of things to "Time Safari"
|
||||||
* Choose Product -> Destination -> Build Any iOS
|
* Choose Product -> Destination -> Any iOS Device
|
||||||
* Choose Product -> Archive
|
* Choose Product -> Archive
|
||||||
* This will trigger a build and take time, needing user's "login" keychain password which is just their login password, repeatedly.
|
* This will trigger a build and take time, needing user's "login" keychain password (user's login password), repeatedly.
|
||||||
* If it fails with `building for 'iOS', but linking in dylib (.../.pkgx/zlib.net/v1.3.0/lib/libz.1.3.dylib) built for 'macOS'` then run XCode outside that terminal (ie. not with `npx cap open ios`).
|
* If it fails with `building for 'iOS', but linking in dylib (.../.pkgx/zlib.net/v1.3.0/lib/libz.1.3.dylib) built for 'macOS'` then run XCode outside that terminal (ie. not with `npx cap open ios`).
|
||||||
* Click Distribute -> App Store Connect
|
* Click Distribute -> App Store Connect
|
||||||
* In AppStoreConnect, add the build to the distribution: remove the current build with the "-" when you hover over it, then "Add Build" with the new build.
|
* In AppStoreConnect, add the build to the distribution: remove the current build with the "-" when you hover over it, then "Add Build" with the new build.
|
||||||
|
* May have to go to App Review, click Submission, then hover over the build and click "-".
|
||||||
* It can take 15 minutes for the build to show up in the list of builds.
|
* It can take 15 minutes for the build to show up in the list of builds.
|
||||||
* You'll probably have to "Manage" something about encryption, disallowed in France.
|
* You'll probably have to "Manage" something about encryption, disallowed in France.
|
||||||
* Then "Save" and "Add to Review" and "Resubmit to App Review".
|
* Then "Save" and "Add to Review" and "Resubmit to App Review".
|
||||||
|
|
||||||
#### First-time iOS Configuration
|
|
||||||
|
|
||||||
- Generate certificates inside XCode.
|
|
||||||
|
|
||||||
- Right-click on App and under Signing & Capabilities set the Team.
|
|
||||||
|
|
||||||
### Android Build
|
### Android Build
|
||||||
|
|
||||||
Prerequisites: Android Studio with SDK installed
|
Prerequisites: Android Studio with Java SDK installed
|
||||||
|
|
||||||
1. Build the web assets:
|
1. Build the web assets:
|
||||||
|
|
||||||
@@ -447,7 +460,9 @@ Prerequisites: Android Studio with SDK installed
|
|||||||
* Then `bundleRelease`:
|
* Then `bundleRelease`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
cd android
|
||||||
./gradlew bundleRelease -Dlint.baselines.continue=true
|
./gradlew bundleRelease -Dlint.baselines.continue=true
|
||||||
|
cd -
|
||||||
```
|
```
|
||||||
|
|
||||||
... and find your `aab` file at app/build/outputs/bundle/release
|
... and find your `aab` file at app/build/outputs/bundle/release
|
||||||
@@ -460,6 +475,8 @@ At play.google.com/console:
|
|||||||
- Hit "Next".
|
- Hit "Next".
|
||||||
- Save, go to "Publishing Overview" as prompted, and click "Send changes for review".
|
- Save, go to "Publishing Overview" as prompted, and click "Send changes for review".
|
||||||
|
|
||||||
|
- Note that if you add testers, you have to go to "Publishing Overview" and send those changes or your (closed) testers won't see it.
|
||||||
|
|
||||||
|
|
||||||
## First-time Android Configuration for deep links
|
## First-time Android Configuration for deep links
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [0.4.7]
|
||||||
|
### Fixed
|
||||||
|
- Cameras everywhere
|
||||||
|
### Changed
|
||||||
|
- IndexedDB -> SQLite
|
||||||
|
|
||||||
|
|
||||||
## [0.4.5] - 2025.02.23
|
## [0.4.5] - 2025.02.23
|
||||||
### Added
|
### Added
|
||||||
- Total amounts of gives on project page
|
- Total amounts of gives on project page
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ android {
|
|||||||
applicationId "app.timesafari.app"
|
applicationId "app.timesafari.app"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 18
|
versionCode 25
|
||||||
versionName "0.4.7"
|
versionName "0.5.0"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
@@ -91,6 +91,8 @@ dependencies {
|
|||||||
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
||||||
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
|
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
|
||||||
implementation project(':capacitor-android')
|
implementation project(':capacitor-android')
|
||||||
|
implementation project(':capacitor-community-sqlite')
|
||||||
|
implementation "androidx.biometric:biometric:1.2.0-alpha05"
|
||||||
testImplementation "junit:junit:$junitVersion"
|
testImplementation "junit:junit:$junitVersion"
|
||||||
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
||||||
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ android {
|
|||||||
|
|
||||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation project(':capacitor-community-sqlite')
|
||||||
implementation project(':capacitor-mlkit-barcode-scanning')
|
implementation project(':capacitor-mlkit-barcode-scanning')
|
||||||
implementation project(':capacitor-app')
|
implementation project(':capacitor-app')
|
||||||
implementation project(':capacitor-camera')
|
implementation project(':capacitor-camera')
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"appId": "app.timesafari.app",
|
"appId": "app.timesafari",
|
||||||
"appName": "TimeSafari",
|
"appName": "TimeSafari",
|
||||||
"webDir": "dist",
|
"webDir": "dist",
|
||||||
"bundledWebRuntime": false,
|
"bundledWebRuntime": false,
|
||||||
@@ -16,6 +16,41 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"SQLite": {
|
||||||
|
"iosDatabaseLocation": "Library/CapacitorDatabase",
|
||||||
|
"iosIsEncryption": true,
|
||||||
|
"iosBiometric": {
|
||||||
|
"biometricAuth": true,
|
||||||
|
"biometricTitle": "Biometric login for TimeSafari"
|
||||||
|
},
|
||||||
|
"androidIsEncryption": true,
|
||||||
|
"androidBiometric": {
|
||||||
|
"biometricAuth": true,
|
||||||
|
"biometricTitle": "Biometric login for TimeSafari"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"ios": {
|
||||||
|
"contentInset": "never",
|
||||||
|
"allowsLinkPreview": true,
|
||||||
|
"scrollEnabled": true,
|
||||||
|
"limitsNavigationsToAppBoundDomains": true,
|
||||||
|
"backgroundColor": "#ffffff",
|
||||||
|
"allowNavigation": [
|
||||||
|
"*.timesafari.app",
|
||||||
|
"*.jsdelivr.net",
|
||||||
|
"api.endorser.ch"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"android": {
|
||||||
|
"allowMixedContent": false,
|
||||||
|
"captureInput": true,
|
||||||
|
"webContentsDebuggingEnabled": false,
|
||||||
|
"allowNavigation": [
|
||||||
|
"*.timesafari.app",
|
||||||
|
"*.jsdelivr.net",
|
||||||
|
"api.endorser.ch"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"pkg": "@capacitor-community/sqlite",
|
||||||
|
"classpath": "com.getcapacitor.community.database.sqlite.CapacitorSQLitePlugin"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"pkg": "@capacitor-mlkit/barcode-scanning",
|
"pkg": "@capacitor-mlkit/barcode-scanning",
|
||||||
"classpath": "io.capawesome.capacitorjs.plugins.mlkit.barcodescanning.BarcodeScannerPlugin"
|
"classpath": "io.capawesome.capacitorjs.plugins.mlkit.barcodescanning.BarcodeScannerPlugin"
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
package app.timesafari;
|
package app.timesafari;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
import com.getcapacitor.BridgeActivity;
|
import com.getcapacitor.BridgeActivity;
|
||||||
|
//import com.getcapacitor.community.sqlite.SQLite;
|
||||||
|
|
||||||
public class MainActivity extends BridgeActivity {
|
public class MainActivity extends BridgeActivity {
|
||||||
// ... existing code ...
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
// Initialize SQLite
|
||||||
|
//registerPlugin(SQLite.class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package timesafari.app;
|
|
||||||
|
|
||||||
import com.getcapacitor.BridgeActivity;
|
|
||||||
|
|
||||||
public class MainActivity extends BridgeActivity {}
|
|
||||||
@@ -2,6 +2,9 @@
|
|||||||
include ':capacitor-android'
|
include ':capacitor-android'
|
||||||
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
||||||
|
|
||||||
|
include ':capacitor-community-sqlite'
|
||||||
|
project(':capacitor-community-sqlite').projectDir = new File('../node_modules/@capacitor-community/sqlite/android')
|
||||||
|
|
||||||
include ':capacitor-mlkit-barcode-scanning'
|
include ':capacitor-mlkit-barcode-scanning'
|
||||||
project(':capacitor-mlkit-barcode-scanning').projectDir = new File('../node_modules/@capacitor-mlkit/barcode-scanning/android')
|
project(':capacitor-mlkit-barcode-scanning').projectDir = new File('../node_modules/@capacitor-mlkit/barcode-scanning/android')
|
||||||
|
|
||||||
|
|||||||
BIN
assets/icon.png
BIN
assets/icon.png
Binary file not shown.
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 279 KiB |
BIN
assets/splash-dark.png
Normal file
BIN
assets/splash-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
BIN
assets/splash.png
Normal file
BIN
assets/splash.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
@@ -16,6 +16,41 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"SQLite": {
|
||||||
|
"iosDatabaseLocation": "Library/CapacitorDatabase",
|
||||||
|
"iosIsEncryption": true,
|
||||||
|
"iosBiometric": {
|
||||||
|
"biometricAuth": true,
|
||||||
|
"biometricTitle": "Biometric login for TimeSafari"
|
||||||
|
},
|
||||||
|
"androidIsEncryption": true,
|
||||||
|
"androidBiometric": {
|
||||||
|
"biometricAuth": true,
|
||||||
|
"biometricTitle": "Biometric login for TimeSafari"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"ios": {
|
||||||
|
"contentInset": "never",
|
||||||
|
"allowsLinkPreview": true,
|
||||||
|
"scrollEnabled": true,
|
||||||
|
"limitsNavigationsToAppBoundDomains": true,
|
||||||
|
"backgroundColor": "#ffffff",
|
||||||
|
"allowNavigation": [
|
||||||
|
"*.timesafari.app",
|
||||||
|
"*.jsdelivr.net",
|
||||||
|
"api.endorser.ch"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"android": {
|
||||||
|
"allowMixedContent": false,
|
||||||
|
"captureInput": true,
|
||||||
|
"webContentsDebuggingEnabled": false,
|
||||||
|
"allowNavigation": [
|
||||||
|
"*.timesafari.app",
|
||||||
|
"*.jsdelivr.net",
|
||||||
|
"api.endorser.ch"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
399
doc/dexie-to-sqlite-mapping.md
Normal file
399
doc/dexie-to-sqlite-mapping.md
Normal file
@@ -0,0 +1,399 @@
|
|||||||
|
# Dexie to absurd-sql Mapping Guide
|
||||||
|
|
||||||
|
## Schema Mapping
|
||||||
|
|
||||||
|
### Current Dexie Schema
|
||||||
|
```typescript
|
||||||
|
// Current Dexie schema
|
||||||
|
const db = new Dexie('TimeSafariDB');
|
||||||
|
|
||||||
|
db.version(1).stores({
|
||||||
|
accounts: 'did, publicKeyHex, createdAt, updatedAt',
|
||||||
|
settings: 'key, value, updatedAt',
|
||||||
|
contacts: 'id, did, name, createdAt, updatedAt'
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### New SQLite Schema
|
||||||
|
```sql
|
||||||
|
-- New SQLite schema
|
||||||
|
CREATE TABLE accounts (
|
||||||
|
did TEXT PRIMARY KEY,
|
||||||
|
public_key_hex TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE settings (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE contacts (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
did TEXT NOT NULL,
|
||||||
|
name TEXT,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (did) REFERENCES accounts(did)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes for performance
|
||||||
|
CREATE INDEX idx_accounts_created_at ON accounts(created_at);
|
||||||
|
CREATE INDEX idx_contacts_did ON contacts(did);
|
||||||
|
CREATE INDEX idx_settings_updated_at ON settings(updated_at);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Query Mapping
|
||||||
|
|
||||||
|
### 1. Account Operations
|
||||||
|
|
||||||
|
#### Get Account by DID
|
||||||
|
```typescript
|
||||||
|
// Dexie
|
||||||
|
const account = await db.accounts.get(did);
|
||||||
|
|
||||||
|
// absurd-sql
|
||||||
|
const result = await db.exec(`
|
||||||
|
SELECT * FROM accounts WHERE did = ?
|
||||||
|
`, [did]);
|
||||||
|
const account = result[0]?.values[0];
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Get All Accounts
|
||||||
|
```typescript
|
||||||
|
// Dexie
|
||||||
|
const accounts = await db.accounts.toArray();
|
||||||
|
|
||||||
|
// absurd-sql
|
||||||
|
const result = await db.exec(`
|
||||||
|
SELECT * FROM accounts ORDER BY created_at DESC
|
||||||
|
`);
|
||||||
|
const accounts = result[0]?.values || [];
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Add Account
|
||||||
|
```typescript
|
||||||
|
// Dexie
|
||||||
|
await db.accounts.add({
|
||||||
|
did,
|
||||||
|
publicKeyHex,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
updatedAt: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
// absurd-sql
|
||||||
|
await db.run(`
|
||||||
|
INSERT INTO accounts (did, public_key_hex, created_at, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
`, [did, publicKeyHex, Date.now(), Date.now()]);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Update Account
|
||||||
|
```typescript
|
||||||
|
// Dexie
|
||||||
|
await db.accounts.update(did, {
|
||||||
|
publicKeyHex,
|
||||||
|
updatedAt: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
// absurd-sql
|
||||||
|
await db.run(`
|
||||||
|
UPDATE accounts
|
||||||
|
SET public_key_hex = ?, updated_at = ?
|
||||||
|
WHERE did = ?
|
||||||
|
`, [publicKeyHex, Date.now(), did]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Settings Operations
|
||||||
|
|
||||||
|
#### Get Setting
|
||||||
|
```typescript
|
||||||
|
// Dexie
|
||||||
|
const setting = await db.settings.get(key);
|
||||||
|
|
||||||
|
// absurd-sql
|
||||||
|
const result = await db.exec(`
|
||||||
|
SELECT * FROM settings WHERE key = ?
|
||||||
|
`, [key]);
|
||||||
|
const setting = result[0]?.values[0];
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Set Setting
|
||||||
|
```typescript
|
||||||
|
// Dexie
|
||||||
|
await db.settings.put({
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
updatedAt: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
// absurd-sql
|
||||||
|
await db.run(`
|
||||||
|
INSERT INTO settings (key, value, updated_at)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
ON CONFLICT(key) DO UPDATE SET
|
||||||
|
value = excluded.value,
|
||||||
|
updated_at = excluded.updated_at
|
||||||
|
`, [key, value, Date.now()]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Contact Operations
|
||||||
|
|
||||||
|
#### Get Contacts by Account
|
||||||
|
```typescript
|
||||||
|
// Dexie
|
||||||
|
const contacts = await db.contacts
|
||||||
|
.where('did')
|
||||||
|
.equals(accountDid)
|
||||||
|
.toArray();
|
||||||
|
|
||||||
|
// absurd-sql
|
||||||
|
const result = await db.exec(`
|
||||||
|
SELECT * FROM contacts
|
||||||
|
WHERE did = ?
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
`, [accountDid]);
|
||||||
|
const contacts = result[0]?.values || [];
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Add Contact
|
||||||
|
```typescript
|
||||||
|
// Dexie
|
||||||
|
await db.contacts.add({
|
||||||
|
id: generateId(),
|
||||||
|
did: accountDid,
|
||||||
|
name,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
updatedAt: Date.now()
|
||||||
|
});
|
||||||
|
|
||||||
|
// absurd-sql
|
||||||
|
await db.run(`
|
||||||
|
INSERT INTO contacts (id, did, name, created_at, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
`, [generateId(), accountDid, name, Date.now(), Date.now()]);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Transaction Mapping
|
||||||
|
|
||||||
|
### Batch Operations
|
||||||
|
```typescript
|
||||||
|
// Dexie
|
||||||
|
await db.transaction('rw', [db.accounts, db.contacts], async () => {
|
||||||
|
await db.accounts.add(account);
|
||||||
|
await db.contacts.bulkAdd(contacts);
|
||||||
|
});
|
||||||
|
|
||||||
|
// absurd-sql
|
||||||
|
await db.exec('BEGIN TRANSACTION;');
|
||||||
|
try {
|
||||||
|
await db.run(`
|
||||||
|
INSERT INTO accounts (did, public_key_hex, created_at, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
`, [account.did, account.publicKeyHex, account.createdAt, account.updatedAt]);
|
||||||
|
|
||||||
|
for (const contact of contacts) {
|
||||||
|
await db.run(`
|
||||||
|
INSERT INTO contacts (id, did, name, created_at, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
`, [contact.id, contact.did, contact.name, contact.createdAt, contact.updatedAt]);
|
||||||
|
}
|
||||||
|
await db.exec('COMMIT;');
|
||||||
|
} catch (error) {
|
||||||
|
await db.exec('ROLLBACK;');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Helper Functions
|
||||||
|
|
||||||
|
### 1. Data Export (Dexie to JSON)
|
||||||
|
```typescript
|
||||||
|
async function exportDexieData(): Promise<MigrationData> {
|
||||||
|
const db = new Dexie('TimeSafariDB');
|
||||||
|
|
||||||
|
return {
|
||||||
|
accounts: await db.accounts.toArray(),
|
||||||
|
settings: await db.settings.toArray(),
|
||||||
|
contacts: await db.contacts.toArray(),
|
||||||
|
metadata: {
|
||||||
|
version: '1.0.0',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
dexieVersion: Dexie.version
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Data Import (JSON to absurd-sql)
|
||||||
|
```typescript
|
||||||
|
async function importToAbsurdSql(data: MigrationData): Promise<void> {
|
||||||
|
await db.exec('BEGIN TRANSACTION;');
|
||||||
|
try {
|
||||||
|
// Import accounts
|
||||||
|
for (const account of data.accounts) {
|
||||||
|
await db.run(`
|
||||||
|
INSERT INTO accounts (did, public_key_hex, created_at, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
`, [account.did, account.publicKeyHex, account.createdAt, account.updatedAt]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import settings
|
||||||
|
for (const setting of data.settings) {
|
||||||
|
await db.run(`
|
||||||
|
INSERT INTO settings (key, value, updated_at)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
`, [setting.key, setting.value, setting.updatedAt]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import contacts
|
||||||
|
for (const contact of data.contacts) {
|
||||||
|
await db.run(`
|
||||||
|
INSERT INTO contacts (id, did, name, created_at, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
`, [contact.id, contact.did, contact.name, contact.createdAt, contact.updatedAt]);
|
||||||
|
}
|
||||||
|
await db.exec('COMMIT;');
|
||||||
|
} catch (error) {
|
||||||
|
await db.exec('ROLLBACK;');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Verification
|
||||||
|
```typescript
|
||||||
|
async function verifyMigration(dexieData: MigrationData): Promise<boolean> {
|
||||||
|
// Verify account count
|
||||||
|
const accountResult = await db.exec('SELECT COUNT(*) as count FROM accounts');
|
||||||
|
const accountCount = accountResult[0].values[0][0];
|
||||||
|
if (accountCount !== dexieData.accounts.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify settings count
|
||||||
|
const settingsResult = await db.exec('SELECT COUNT(*) as count FROM settings');
|
||||||
|
const settingsCount = settingsResult[0].values[0][0];
|
||||||
|
if (settingsCount !== dexieData.settings.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify contacts count
|
||||||
|
const contactsResult = await db.exec('SELECT COUNT(*) as count FROM contacts');
|
||||||
|
const contactsCount = contactsResult[0].values[0][0];
|
||||||
|
if (contactsCount !== dexieData.contacts.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify data integrity
|
||||||
|
for (const account of dexieData.accounts) {
|
||||||
|
const result = await db.exec(
|
||||||
|
'SELECT * FROM accounts WHERE did = ?',
|
||||||
|
[account.did]
|
||||||
|
);
|
||||||
|
const migratedAccount = result[0]?.values[0];
|
||||||
|
if (!migratedAccount ||
|
||||||
|
migratedAccount[1] !== account.publicKeyHex) { // public_key_hex is second column
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
### 1. Indexing
|
||||||
|
- Dexie automatically creates indexes based on the schema
|
||||||
|
- absurd-sql requires explicit index creation
|
||||||
|
- Added indexes for frequently queried fields
|
||||||
|
- Use `PRAGMA journal_mode=MEMORY;` for better performance
|
||||||
|
|
||||||
|
### 2. Batch Operations
|
||||||
|
- Dexie has built-in bulk operations
|
||||||
|
- absurd-sql uses transactions for batch operations
|
||||||
|
- Consider chunking large datasets
|
||||||
|
- Use prepared statements for repeated queries
|
||||||
|
|
||||||
|
### 3. Query Optimization
|
||||||
|
- Dexie uses IndexedDB's native indexing
|
||||||
|
- absurd-sql requires explicit query optimization
|
||||||
|
- Use prepared statements for repeated queries
|
||||||
|
- Consider using `PRAGMA synchronous=NORMAL;` for better performance
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### 1. Common Errors
|
||||||
|
```typescript
|
||||||
|
// Dexie errors
|
||||||
|
try {
|
||||||
|
await db.accounts.add(account);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Dexie.ConstraintError) {
|
||||||
|
// Handle duplicate key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// absurd-sql errors
|
||||||
|
try {
|
||||||
|
await db.run(`
|
||||||
|
INSERT INTO accounts (did, public_key_hex, created_at, updated_at)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
`, [account.did, account.publicKeyHex, account.createdAt, account.updatedAt]);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.message.includes('UNIQUE constraint failed')) {
|
||||||
|
// Handle duplicate key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Transaction Recovery
|
||||||
|
```typescript
|
||||||
|
// Dexie transaction
|
||||||
|
try {
|
||||||
|
await db.transaction('rw', db.accounts, async () => {
|
||||||
|
// Operations
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// Dexie automatically rolls back
|
||||||
|
}
|
||||||
|
|
||||||
|
// absurd-sql transaction
|
||||||
|
try {
|
||||||
|
await db.exec('BEGIN TRANSACTION;');
|
||||||
|
// Operations
|
||||||
|
await db.exec('COMMIT;');
|
||||||
|
} catch (error) {
|
||||||
|
await db.exec('ROLLBACK;');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Strategy
|
||||||
|
|
||||||
|
1. **Preparation**
|
||||||
|
- Export all Dexie data
|
||||||
|
- Verify data integrity
|
||||||
|
- Create SQLite schema
|
||||||
|
- Setup indexes
|
||||||
|
|
||||||
|
2. **Migration**
|
||||||
|
- Import data in transactions
|
||||||
|
- Verify each batch
|
||||||
|
- Handle errors gracefully
|
||||||
|
- Maintain backup
|
||||||
|
|
||||||
|
3. **Verification**
|
||||||
|
- Compare record counts
|
||||||
|
- Verify data integrity
|
||||||
|
- Test common queries
|
||||||
|
- Validate relationships
|
||||||
|
|
||||||
|
4. **Cleanup**
|
||||||
|
- Remove Dexie database
|
||||||
|
- Clear IndexedDB storage
|
||||||
|
- Update application code
|
||||||
|
- Remove old dependencies
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
# Migration Guide: Dexie to wa-sqlite
|
# Migration Guide: Dexie to absurd-sql
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
This document outlines the migration process from Dexie.js to wa-sqlite for the TimeSafari app's storage implementation. The migration aims to provide a consistent SQLite-based storage solution across all platforms while maintaining data integrity and ensuring a smooth transition for users.
|
This document outlines the migration process from Dexie.js to absurd-sql for the TimeSafari app's storage implementation. The migration aims to provide a consistent SQLite-based storage solution across all platforms while maintaining data integrity and ensuring a smooth transition for users.
|
||||||
|
|
||||||
## Migration Goals
|
## Migration Goals
|
||||||
|
|
||||||
@@ -43,12 +43,20 @@ This document outlines the migration process from Dexie.js to wa-sqlite for the
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Storage Requirements**
|
2. **Dependencies**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@jlongster/sql.js": "^1.8.0",
|
||||||
|
"absurd-sql": "^1.8.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Storage Requirements**
|
||||||
- Sufficient IndexedDB quota
|
- Sufficient IndexedDB quota
|
||||||
- Available disk space for SQLite
|
- Available disk space for SQLite
|
||||||
- Backup storage space
|
- Backup storage space
|
||||||
|
|
||||||
3. **Platform Support**
|
4. **Platform Support**
|
||||||
- Web: Modern browser with IndexedDB support
|
- Web: Modern browser with IndexedDB support
|
||||||
- iOS: iOS 13+ with SQLite support
|
- iOS: iOS 13+ with SQLite support
|
||||||
- Android: Android 5+ with SQLite support
|
- Android: Android 5+ with SQLite support
|
||||||
@@ -60,9 +68,15 @@ This document outlines the migration process from Dexie.js to wa-sqlite for the
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// src/services/storage/migration/MigrationService.ts
|
// src/services/storage/migration/MigrationService.ts
|
||||||
|
import initSqlJs from '@jlongster/sql.js';
|
||||||
|
import { SQLiteFS } from 'absurd-sql';
|
||||||
|
import IndexedDBBackend from 'absurd-sql/dist/indexeddb-backend';
|
||||||
|
|
||||||
export class MigrationService {
|
export class MigrationService {
|
||||||
private static instance: MigrationService;
|
private static instance: MigrationService;
|
||||||
private backup: MigrationBackup | null = null;
|
private backup: MigrationBackup | null = null;
|
||||||
|
private sql: any = null;
|
||||||
|
private db: any = null;
|
||||||
|
|
||||||
async prepare(): Promise<void> {
|
async prepare(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
@@ -75,8 +89,8 @@ export class MigrationService {
|
|||||||
// 3. Verify backup integrity
|
// 3. Verify backup integrity
|
||||||
await this.verifyBackup();
|
await this.verifyBackup();
|
||||||
|
|
||||||
// 4. Initialize wa-sqlite
|
// 4. Initialize absurd-sql
|
||||||
await this.initializeWaSqlite();
|
await this.initializeAbsurdSql();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new StorageError(
|
throw new StorageError(
|
||||||
'Migration preparation failed',
|
'Migration preparation failed',
|
||||||
@@ -86,6 +100,42 @@ export class MigrationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async initializeAbsurdSql(): Promise<void> {
|
||||||
|
// Initialize SQL.js
|
||||||
|
this.sql = await initSqlJs({
|
||||||
|
locateFile: (file: string) => {
|
||||||
|
return new URL(`/node_modules/@jlongster/sql.js/dist/${file}`, import.meta.url).href;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup SQLiteFS with IndexedDB backend
|
||||||
|
const sqlFS = new SQLiteFS(this.sql.FS, new IndexedDBBackend());
|
||||||
|
this.sql.register_for_idb(sqlFS);
|
||||||
|
|
||||||
|
// Create and mount filesystem
|
||||||
|
this.sql.FS.mkdir('/sql');
|
||||||
|
this.sql.FS.mount(sqlFS, {}, '/sql');
|
||||||
|
|
||||||
|
// Open database
|
||||||
|
const path = '/sql/db.sqlite';
|
||||||
|
if (typeof SharedArrayBuffer === 'undefined') {
|
||||||
|
let stream = this.sql.FS.open(path, 'a+');
|
||||||
|
await stream.node.contents.readIfFallback();
|
||||||
|
this.sql.FS.close(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.db = new this.sql.Database(path, { filename: true });
|
||||||
|
if (!this.db) {
|
||||||
|
throw new StorageError(
|
||||||
|
'Database initialization failed',
|
||||||
|
StorageErrorCodes.INITIALIZATION_FAILED
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure database
|
||||||
|
await this.db.exec(`PRAGMA journal_mode=MEMORY;`);
|
||||||
|
}
|
||||||
|
|
||||||
private async checkPrerequisites(): Promise<void> {
|
private async checkPrerequisites(): Promise<void> {
|
||||||
// Check IndexedDB availability
|
// Check IndexedDB availability
|
||||||
if (!window.indexedDB) {
|
if (!window.indexedDB) {
|
||||||
@@ -160,12 +210,11 @@ export class DataMigration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async migrateAccounts(accounts: Account[]): Promise<void> {
|
private async migrateAccounts(accounts: Account[]): Promise<void> {
|
||||||
const db = await this.getWaSqliteConnection();
|
|
||||||
|
|
||||||
// Use transaction for atomicity
|
// Use transaction for atomicity
|
||||||
await db.transaction(async (tx) => {
|
await this.db.exec('BEGIN TRANSACTION;');
|
||||||
|
try {
|
||||||
for (const account of accounts) {
|
for (const account of accounts) {
|
||||||
await tx.execute(`
|
await this.db.run(`
|
||||||
INSERT INTO accounts (did, public_key_hex, created_at, updated_at)
|
INSERT INTO accounts (did, public_key_hex, created_at, updated_at)
|
||||||
VALUES (?, ?, ?, ?)
|
VALUES (?, ?, ?, ?)
|
||||||
`, [
|
`, [
|
||||||
@@ -175,16 +224,18 @@ export class DataMigration {
|
|||||||
account.updatedAt
|
account.updatedAt
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
});
|
await this.db.exec('COMMIT;');
|
||||||
|
} catch (error) {
|
||||||
|
await this.db.exec('ROLLBACK;');
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async verifyMigration(backup: MigrationBackup): Promise<void> {
|
private async verifyMigration(backup: MigrationBackup): Promise<void> {
|
||||||
const db = await this.getWaSqliteConnection();
|
|
||||||
|
|
||||||
// Verify account count
|
// Verify account count
|
||||||
const accountCount = await db.selectValue(
|
const result = await this.db.exec('SELECT COUNT(*) as count FROM accounts');
|
||||||
'SELECT COUNT(*) FROM accounts'
|
const accountCount = result[0].values[0][0];
|
||||||
);
|
|
||||||
if (accountCount !== backup.accounts.length) {
|
if (accountCount !== backup.accounts.length) {
|
||||||
throw new StorageError(
|
throw new StorageError(
|
||||||
'Account count mismatch',
|
'Account count mismatch',
|
||||||
@@ -214,8 +265,8 @@ export class RollbackService {
|
|||||||
// 3. Verify restoration
|
// 3. Verify restoration
|
||||||
await this.verifyRestoration(backup);
|
await this.verifyRestoration(backup);
|
||||||
|
|
||||||
// 4. Clean up wa-sqlite
|
// 4. Clean up absurd-sql
|
||||||
await this.cleanupWaSqlite();
|
await this.cleanupAbsurdSql();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new StorageError(
|
throw new StorageError(
|
||||||
'Rollback failed',
|
'Rollback failed',
|
||||||
@@ -371,6 +422,14 @@ button:hover {
|
|||||||
```typescript
|
```typescript
|
||||||
// src/services/storage/migration/__tests__/MigrationService.spec.ts
|
// src/services/storage/migration/__tests__/MigrationService.spec.ts
|
||||||
describe('MigrationService', () => {
|
describe('MigrationService', () => {
|
||||||
|
it('should initialize absurd-sql correctly', async () => {
|
||||||
|
const service = MigrationService.getInstance();
|
||||||
|
await service.initializeAbsurdSql();
|
||||||
|
|
||||||
|
expect(service.isInitialized()).toBe(true);
|
||||||
|
expect(service.getDatabase()).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
it('should create valid backup', async () => {
|
it('should create valid backup', async () => {
|
||||||
const service = MigrationService.getInstance();
|
const service = MigrationService.getInstance();
|
||||||
const backup = await service.createBackup();
|
const backup = await service.createBackup();
|
||||||
339
doc/secure-storage-implementation.md
Normal file
339
doc/secure-storage-implementation.md
Normal file
@@ -0,0 +1,339 @@
|
|||||||
|
# Secure Storage Implementation Guide for TimeSafari App
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document outlines the implementation of secure storage for the TimeSafari app. The implementation focuses on:
|
||||||
|
|
||||||
|
1. **Platform-Specific Storage Solutions**:
|
||||||
|
- Web: SQLite with IndexedDB backend (absurd-sql)
|
||||||
|
- Electron: SQLite with Node.js backend
|
||||||
|
- Native: (Planned) SQLCipher with platform-specific secure storage
|
||||||
|
|
||||||
|
2. **Key Features**:
|
||||||
|
- SQLite-based storage using absurd-sql for web
|
||||||
|
- Platform-specific service factory pattern
|
||||||
|
- Consistent API across platforms
|
||||||
|
- Migration support from Dexie.js
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Core dependencies
|
||||||
|
npm install @jlongster/sql.js
|
||||||
|
npm install absurd-sql
|
||||||
|
|
||||||
|
# Platform-specific dependencies (for future native support)
|
||||||
|
npm install @capacitor/preferences
|
||||||
|
npm install @capacitor-community/biometric-auth
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Basic Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Using the platform service
|
||||||
|
import { PlatformServiceFactory } from '../services/PlatformServiceFactory';
|
||||||
|
|
||||||
|
// Get platform-specific service instance
|
||||||
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
|
|
||||||
|
// Example database operations
|
||||||
|
async function example() {
|
||||||
|
try {
|
||||||
|
// Query example
|
||||||
|
const result = await platformService.dbQuery(
|
||||||
|
"SELECT * FROM accounts WHERE did = ?",
|
||||||
|
[did]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Execute example
|
||||||
|
await platformService.dbExec(
|
||||||
|
"INSERT INTO accounts (did, public_key_hex) VALUES (?, ?)",
|
||||||
|
[did, publicKeyHex]
|
||||||
|
);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Database operation failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Platform Detection
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/services/PlatformServiceFactory.ts
|
||||||
|
export class PlatformServiceFactory {
|
||||||
|
static getInstance(): PlatformService {
|
||||||
|
if (process.env.ELECTRON) {
|
||||||
|
// Electron platform
|
||||||
|
return new ElectronPlatformService();
|
||||||
|
} else {
|
||||||
|
// Web platform (default)
|
||||||
|
return new AbsurdSqlDatabaseService();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Current Implementation Details
|
||||||
|
|
||||||
|
#### Web Platform (AbsurdSqlDatabaseService)
|
||||||
|
|
||||||
|
The web platform uses absurd-sql with IndexedDB backend:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/services/AbsurdSqlDatabaseService.ts
|
||||||
|
export class AbsurdSqlDatabaseService implements PlatformService {
|
||||||
|
private static instance: AbsurdSqlDatabaseService | null = null;
|
||||||
|
private db: AbsurdSqlDatabase | null = null;
|
||||||
|
private initialized: boolean = false;
|
||||||
|
|
||||||
|
// Singleton pattern
|
||||||
|
static getInstance(): AbsurdSqlDatabaseService {
|
||||||
|
if (!AbsurdSqlDatabaseService.instance) {
|
||||||
|
AbsurdSqlDatabaseService.instance = new AbsurdSqlDatabaseService();
|
||||||
|
}
|
||||||
|
return AbsurdSqlDatabaseService.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Database operations
|
||||||
|
async dbQuery(sql: string, params: unknown[] = []): Promise<QueryExecResult[]> {
|
||||||
|
await this.waitForInitialization();
|
||||||
|
return this.queueOperation<QueryExecResult[]>("query", sql, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
async dbExec(sql: string, params: unknown[] = []): Promise<void> {
|
||||||
|
await this.waitForInitialization();
|
||||||
|
await this.queueOperation<void>("run", sql, params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Key features:
|
||||||
|
- Uses absurd-sql for SQLite in the browser
|
||||||
|
- Implements operation queuing for thread safety
|
||||||
|
- Handles initialization and connection management
|
||||||
|
- Provides consistent API across platforms
|
||||||
|
|
||||||
|
### 5. Migration from Dexie.js
|
||||||
|
|
||||||
|
The current implementation supports gradual migration from Dexie.js:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Example of dual-storage pattern
|
||||||
|
async function getAccount(did: string): Promise<Account | undefined> {
|
||||||
|
// Try SQLite first
|
||||||
|
const platform = PlatformServiceFactory.getInstance();
|
||||||
|
let account = await platform.dbQuery(
|
||||||
|
"SELECT * FROM accounts WHERE did = ?",
|
||||||
|
[did]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fallback to Dexie if needed
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
account = await db.accounts.get(did);
|
||||||
|
}
|
||||||
|
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### A. Modifying Code
|
||||||
|
|
||||||
|
When converting from Dexie.js to SQL-based implementation, follow these patterns:
|
||||||
|
|
||||||
|
1. **Database Access Pattern**
|
||||||
|
```typescript
|
||||||
|
// Before (Dexie)
|
||||||
|
const result = await db.table.where("field").equals(value).first();
|
||||||
|
|
||||||
|
// After (SQL)
|
||||||
|
const platform = PlatformServiceFactory.getInstance();
|
||||||
|
let result = await platform.dbQuery(
|
||||||
|
"SELECT * FROM table WHERE field = ?",
|
||||||
|
[value]
|
||||||
|
);
|
||||||
|
result = databaseUtil.mapQueryResultToValues(result);
|
||||||
|
|
||||||
|
// Fallback to Dexie if needed
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
result = await db.table.where("field").equals(value).first();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Update Operations**
|
||||||
|
```typescript
|
||||||
|
// Before (Dexie)
|
||||||
|
await db.table.where("id").equals(id).modify(changes);
|
||||||
|
|
||||||
|
// After (SQL)
|
||||||
|
// For settings updates, use the utility methods:
|
||||||
|
await databaseUtil.updateDefaultSettings(changes);
|
||||||
|
// OR
|
||||||
|
await databaseUtil.updateAccountSettings(did, changes);
|
||||||
|
|
||||||
|
// For other tables, use direct SQL:
|
||||||
|
const platform = PlatformServiceFactory.getInstance();
|
||||||
|
await platform.dbExec(
|
||||||
|
"UPDATE table SET field1 = ?, field2 = ? WHERE id = ?",
|
||||||
|
[changes.field1, changes.field2, id]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fallback to Dexie if needed
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
await db.table.where("id").equals(id).modify(changes);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Insert Operations**
|
||||||
|
```typescript
|
||||||
|
// Before (Dexie)
|
||||||
|
await db.table.add(item);
|
||||||
|
|
||||||
|
// After (SQL)
|
||||||
|
const platform = PlatformServiceFactory.getInstance();
|
||||||
|
const columns = Object.keys(item);
|
||||||
|
const values = Object.values(item);
|
||||||
|
const placeholders = values.map(() => '?').join(', ');
|
||||||
|
const sql = `INSERT INTO table (${columns.join(', ')}) VALUES (${placeholders})`;
|
||||||
|
await platform.dbExec(sql, values);
|
||||||
|
|
||||||
|
// Fallback to Dexie if needed
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
await db.table.add(item);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Delete Operations**
|
||||||
|
```typescript
|
||||||
|
// Before (Dexie)
|
||||||
|
await db.table.where("id").equals(id).delete();
|
||||||
|
|
||||||
|
// After (SQL)
|
||||||
|
const platform = PlatformServiceFactory.getInstance();
|
||||||
|
await platform.dbExec("DELETE FROM table WHERE id = ?", [id]);
|
||||||
|
|
||||||
|
// Fallback to Dexie if needed
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
await db.table.where("id").equals(id).delete();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Result Processing**
|
||||||
|
```typescript
|
||||||
|
// Before (Dexie)
|
||||||
|
const items = await db.table.toArray();
|
||||||
|
|
||||||
|
// After (SQL)
|
||||||
|
const platform = PlatformServiceFactory.getInstance();
|
||||||
|
let items = await platform.dbQuery("SELECT * FROM table");
|
||||||
|
items = databaseUtil.mapQueryResultToValues(items);
|
||||||
|
|
||||||
|
// Fallback to Dexie if needed
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
items = await db.table.toArray();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Using Utility Methods**
|
||||||
|
|
||||||
|
When working with settings or other common operations, use the utility methods in `db/index.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Settings operations
|
||||||
|
await databaseUtil.updateDefaultSettings(settings);
|
||||||
|
await databaseUtil.updateAccountSettings(did, settings);
|
||||||
|
const settings = await databaseUtil.retrieveSettingsForDefaultAccount();
|
||||||
|
const settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||||
|
|
||||||
|
// Logging operations
|
||||||
|
await databaseUtil.logToDb(message);
|
||||||
|
await databaseUtil.logConsoleAndDb(message, showInConsole);
|
||||||
|
```
|
||||||
|
|
||||||
|
Key Considerations:
|
||||||
|
- Always use `databaseUtil.mapQueryResultToValues()` to process SQL query results
|
||||||
|
- Use utility methods from `db/index.ts` when available instead of direct SQL
|
||||||
|
- Keep Dexie fallbacks wrapped in `if (USE_DEXIE_DB)` checks
|
||||||
|
- For queries that return results, use `let` variables to allow Dexie fallback to override
|
||||||
|
- For updates/inserts/deletes, execute both SQL and Dexie operations when `USE_DEXIE_DB` is true
|
||||||
|
|
||||||
|
Example Migration:
|
||||||
|
```typescript
|
||||||
|
// Before (Dexie)
|
||||||
|
export async function updateSettings(settings: Settings): Promise<void> {
|
||||||
|
await db.settings.put(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
// After (SQL)
|
||||||
|
export async function updateSettings(settings: Settings): Promise<void> {
|
||||||
|
const platform = PlatformServiceFactory.getInstance();
|
||||||
|
const { sql, params } = generateUpdateStatement(
|
||||||
|
settings,
|
||||||
|
"settings",
|
||||||
|
"id = ?",
|
||||||
|
[settings.id]
|
||||||
|
);
|
||||||
|
await platform.dbExec(sql, params);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Remember to:
|
||||||
|
- Create database access code to use the platform service, putting it in front of the Dexie version
|
||||||
|
- Instead of removing Dexie-specific code, keep it.
|
||||||
|
|
||||||
|
- For creates & updates & deletes, the duplicate code is fine.
|
||||||
|
|
||||||
|
- For queries where we use the results, make the setting from SQL into a 'let' variable, then wrap the Dexie code in a check for USE_DEXIE_DB from app.ts and if
|
||||||
|
it's true then use that result instead of the SQL code's result.
|
||||||
|
|
||||||
|
- Consider data migration needs, and warn if there are any potential migration problems
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
1. **Functionality**
|
||||||
|
- [x] Basic CRUD operations work correctly
|
||||||
|
- [x] Platform service factory pattern implemented
|
||||||
|
- [x] Error handling in place
|
||||||
|
- [ ] Native platform support (planned)
|
||||||
|
|
||||||
|
2. **Performance**
|
||||||
|
- [x] Database operations complete within acceptable time
|
||||||
|
- [x] Operation queuing for thread safety
|
||||||
|
- [x] Proper initialization handling
|
||||||
|
- [ ] Performance monitoring (planned)
|
||||||
|
|
||||||
|
3. **Security**
|
||||||
|
- [x] Basic data integrity
|
||||||
|
- [ ] Encryption (planned for native platforms)
|
||||||
|
- [ ] Secure key storage (planned)
|
||||||
|
- [ ] Platform-specific security features (planned)
|
||||||
|
|
||||||
|
4. **Testing**
|
||||||
|
- [x] Basic unit tests
|
||||||
|
- [ ] Comprehensive integration tests (planned)
|
||||||
|
- [ ] Platform-specific tests (planned)
|
||||||
|
- [ ] Migration tests (planned)
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Native Platform Support**
|
||||||
|
- Implement SQLCipher for iOS/Android
|
||||||
|
- Add platform-specific secure storage
|
||||||
|
- Implement biometric authentication
|
||||||
|
|
||||||
|
2. **Enhanced Security**
|
||||||
|
- Add encryption for sensitive data
|
||||||
|
- Implement secure key storage
|
||||||
|
- Add platform-specific security features
|
||||||
|
|
||||||
|
3. **Testing and Monitoring**
|
||||||
|
- Add comprehensive test coverage
|
||||||
|
- Implement performance monitoring
|
||||||
|
- Add error tracking and analytics
|
||||||
|
|
||||||
|
4. **Documentation**
|
||||||
|
- Add API documentation
|
||||||
|
- Create migration guides
|
||||||
|
- Document security measures
|
||||||
329
doc/storage-implementation-checklist.md
Normal file
329
doc/storage-implementation-checklist.md
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
# Storage Implementation Checklist
|
||||||
|
|
||||||
|
## Core Services
|
||||||
|
|
||||||
|
### 1. Storage Service Layer
|
||||||
|
- [x] Create base `PlatformService` interface
|
||||||
|
- [x] Define common methods for all platforms
|
||||||
|
- [x] Add platform-specific method signatures
|
||||||
|
- [x] Include error handling types
|
||||||
|
- [x] Add migration support methods
|
||||||
|
|
||||||
|
- [x] Implement platform-specific services
|
||||||
|
- [x] `AbsurdSqlDatabaseService` (web)
|
||||||
|
- [x] Database initialization
|
||||||
|
- [x] VFS setup with IndexedDB backend
|
||||||
|
- [x] Connection management
|
||||||
|
- [x] Operation queuing
|
||||||
|
- [ ] `NativeSQLiteService` (iOS/Android) (planned)
|
||||||
|
- [ ] SQLCipher integration
|
||||||
|
- [ ] Native bridge setup
|
||||||
|
- [ ] File system access
|
||||||
|
- [ ] `ElectronSQLiteService` (planned)
|
||||||
|
- [ ] Node SQLite integration
|
||||||
|
- [ ] IPC communication
|
||||||
|
- [ ] File system access
|
||||||
|
|
||||||
|
### 2. Migration Services
|
||||||
|
- [x] Implement basic migration support
|
||||||
|
- [x] Dual-storage pattern (SQLite + Dexie)
|
||||||
|
- [x] Basic data verification
|
||||||
|
- [ ] Rollback procedures (planned)
|
||||||
|
- [ ] Progress tracking (planned)
|
||||||
|
- [ ] Create `MigrationUI` components (planned)
|
||||||
|
- [ ] Progress indicators
|
||||||
|
- [ ] Error handling
|
||||||
|
- [ ] User notifications
|
||||||
|
- [ ] Manual triggers
|
||||||
|
|
||||||
|
### 3. Security Layer
|
||||||
|
- [x] Basic data integrity
|
||||||
|
- [ ] Implement `EncryptionService` (planned)
|
||||||
|
- [ ] Key management
|
||||||
|
- [ ] Encryption/decryption
|
||||||
|
- [ ] Secure storage
|
||||||
|
- [ ] Add `BiometricService` (planned)
|
||||||
|
- [ ] Platform detection
|
||||||
|
- [ ] Authentication flow
|
||||||
|
- [ ] Fallback mechanisms
|
||||||
|
|
||||||
|
## Platform-Specific Implementation
|
||||||
|
|
||||||
|
### Web Platform
|
||||||
|
- [x] Setup absurd-sql
|
||||||
|
- [x] Install dependencies
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"@jlongster/sql.js": "^1.8.0",
|
||||||
|
"absurd-sql": "^1.8.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- [x] Configure VFS with IndexedDB backend
|
||||||
|
- [x] Setup worker threads
|
||||||
|
- [x] Implement operation queuing
|
||||||
|
- [x] Configure database pragmas
|
||||||
|
|
||||||
|
```sql
|
||||||
|
PRAGMA journal_mode=MEMORY;
|
||||||
|
PRAGMA synchronous=NORMAL;
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
|
PRAGMA busy_timeout=5000;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] Update build configuration
|
||||||
|
- [x] Modify `vite.config.ts`
|
||||||
|
- [x] Add worker configuration
|
||||||
|
- [x] Update chunk splitting
|
||||||
|
- [x] Configure asset handling
|
||||||
|
|
||||||
|
- [x] Implement IndexedDB backend
|
||||||
|
- [x] Create database service
|
||||||
|
- [x] Add operation queuing
|
||||||
|
- [x] Handle initialization
|
||||||
|
- [x] Implement atomic operations
|
||||||
|
|
||||||
|
### iOS Platform (Planned)
|
||||||
|
- [ ] Setup SQLCipher
|
||||||
|
- [ ] Install pod dependencies
|
||||||
|
- [ ] Configure encryption
|
||||||
|
- [ ] Setup keychain access
|
||||||
|
- [ ] Implement secure storage
|
||||||
|
|
||||||
|
- [ ] Update Capacitor config
|
||||||
|
- [ ] Modify `capacitor.config.ts`
|
||||||
|
- [ ] Add iOS permissions
|
||||||
|
- [ ] Configure backup
|
||||||
|
- [ ] Setup app groups
|
||||||
|
|
||||||
|
### Android Platform (Planned)
|
||||||
|
- [ ] Setup SQLCipher
|
||||||
|
- [ ] Add Gradle dependencies
|
||||||
|
- [ ] Configure encryption
|
||||||
|
- [ ] Setup keystore
|
||||||
|
- [ ] Implement secure storage
|
||||||
|
|
||||||
|
- [ ] Update Capacitor config
|
||||||
|
- [ ] Modify `capacitor.config.ts`
|
||||||
|
- [ ] Add Android permissions
|
||||||
|
- [ ] Configure backup
|
||||||
|
- [ ] Setup file provider
|
||||||
|
|
||||||
|
### Electron Platform (Planned)
|
||||||
|
- [ ] Setup Node SQLite
|
||||||
|
- [ ] Install dependencies
|
||||||
|
- [ ] Configure IPC
|
||||||
|
- [ ] Setup file system access
|
||||||
|
- [ ] Implement secure storage
|
||||||
|
|
||||||
|
- [ ] Update Electron config
|
||||||
|
- [ ] Modify `electron.config.ts`
|
||||||
|
- [ ] Add security policies
|
||||||
|
- [ ] Configure file access
|
||||||
|
- [ ] Setup auto-updates
|
||||||
|
|
||||||
|
## Data Models and Types
|
||||||
|
|
||||||
|
### 1. Database Schema
|
||||||
|
- [x] Define tables
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Accounts table
|
||||||
|
CREATE TABLE accounts (
|
||||||
|
did TEXT PRIMARY KEY,
|
||||||
|
public_key_hex TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Settings table
|
||||||
|
CREATE TABLE settings (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Contacts table
|
||||||
|
CREATE TABLE contacts (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
did TEXT NOT NULL,
|
||||||
|
name TEXT,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (did) REFERENCES accounts(did)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes for performance
|
||||||
|
CREATE INDEX idx_accounts_created_at ON accounts(created_at);
|
||||||
|
CREATE INDEX idx_contacts_did ON contacts(did);
|
||||||
|
CREATE INDEX idx_settings_updated_at ON settings(updated_at);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] Create indexes
|
||||||
|
- [x] Define constraints
|
||||||
|
- [ ] Add triggers (planned)
|
||||||
|
- [ ] Setup migrations (planned)
|
||||||
|
|
||||||
|
### 2. Type Definitions
|
||||||
|
|
||||||
|
- [x] Create interfaces
|
||||||
|
```typescript
|
||||||
|
interface Account {
|
||||||
|
did: string;
|
||||||
|
publicKeyHex: string;
|
||||||
|
createdAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Setting {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
updatedAt: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Contact {
|
||||||
|
id: string;
|
||||||
|
did: string;
|
||||||
|
name?: string;
|
||||||
|
createdAt: number;
|
||||||
|
updatedAt: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [x] Add validation
|
||||||
|
- [x] Create DTOs
|
||||||
|
- [x] Define enums
|
||||||
|
- [x] Add type guards
|
||||||
|
|
||||||
|
## UI Components
|
||||||
|
|
||||||
|
### 1. Migration UI (Planned)
|
||||||
|
- [ ] Create components
|
||||||
|
- [ ] `MigrationProgress.vue`
|
||||||
|
- [ ] `MigrationError.vue`
|
||||||
|
- [ ] `MigrationSettings.vue`
|
||||||
|
- [ ] `MigrationStatus.vue`
|
||||||
|
|
||||||
|
### 2. Settings UI (Planned)
|
||||||
|
- [ ] Update components
|
||||||
|
- [ ] Add storage settings
|
||||||
|
- [ ] Add migration controls
|
||||||
|
- [ ] Add backup options
|
||||||
|
- [ ] Add security settings
|
||||||
|
|
||||||
|
### 3. Error Handling UI (Planned)
|
||||||
|
- [ ] Create components
|
||||||
|
- [ ] `StorageError.vue`
|
||||||
|
- [ ] `QuotaExceeded.vue`
|
||||||
|
- [ ] `MigrationFailed.vue`
|
||||||
|
- [ ] `RecoveryOptions.vue`
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### 1. Unit Tests
|
||||||
|
- [x] Basic service tests
|
||||||
|
- [x] Platform service tests
|
||||||
|
- [x] Database operation tests
|
||||||
|
- [ ] Security service tests (planned)
|
||||||
|
- [ ] Platform detection tests (planned)
|
||||||
|
|
||||||
|
### 2. Integration Tests (Planned)
|
||||||
|
- [ ] Test migrations
|
||||||
|
- [ ] Web platform tests
|
||||||
|
- [ ] iOS platform tests
|
||||||
|
- [ ] Android platform tests
|
||||||
|
- [ ] Electron platform tests
|
||||||
|
|
||||||
|
### 3. E2E Tests (Planned)
|
||||||
|
- [ ] Test workflows
|
||||||
|
- [ ] Account management
|
||||||
|
- [ ] Settings management
|
||||||
|
- [ ] Contact management
|
||||||
|
- [ ] Migration process
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
### 1. Technical Documentation
|
||||||
|
- [x] Update architecture docs
|
||||||
|
- [x] Add API documentation
|
||||||
|
- [ ] Create migration guides (planned)
|
||||||
|
- [ ] Document security measures (planned)
|
||||||
|
|
||||||
|
### 2. User Documentation (Planned)
|
||||||
|
- [ ] Update user guides
|
||||||
|
- [ ] Add troubleshooting guides
|
||||||
|
- [ ] Create FAQ
|
||||||
|
- [ ] Document new features
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### 1. Build Process
|
||||||
|
- [x] Update build scripts
|
||||||
|
- [x] Add platform-specific builds
|
||||||
|
- [ ] Configure CI/CD (planned)
|
||||||
|
- [ ] Setup automated testing (planned)
|
||||||
|
|
||||||
|
### 2. Release Process (Planned)
|
||||||
|
- [ ] Create release checklist
|
||||||
|
- [ ] Add version management
|
||||||
|
- [ ] Setup rollback procedures
|
||||||
|
- [ ] Configure monitoring
|
||||||
|
|
||||||
|
## Monitoring and Analytics (Planned)
|
||||||
|
|
||||||
|
### 1. Error Tracking
|
||||||
|
- [ ] Setup error logging
|
||||||
|
- [ ] Add performance monitoring
|
||||||
|
- [ ] Configure alerts
|
||||||
|
- [ ] Create dashboards
|
||||||
|
|
||||||
|
### 2. Usage Analytics
|
||||||
|
- [ ] Add storage metrics
|
||||||
|
- [ ] Track migration success
|
||||||
|
- [ ] Monitor performance
|
||||||
|
- [ ] Collect user feedback
|
||||||
|
|
||||||
|
## Security Audit (Planned)
|
||||||
|
|
||||||
|
### 1. Code Review
|
||||||
|
- [ ] Review encryption
|
||||||
|
- [ ] Check access controls
|
||||||
|
- [ ] Verify data handling
|
||||||
|
- [ ] Audit dependencies
|
||||||
|
|
||||||
|
### 2. Penetration Testing
|
||||||
|
- [ ] Test data access
|
||||||
|
- [ ] Verify encryption
|
||||||
|
- [ ] Check authentication
|
||||||
|
- [ ] Review permissions
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
### 1. Performance
|
||||||
|
- [x] Query response time < 100ms
|
||||||
|
- [x] Operation queuing for thread safety
|
||||||
|
- [x] Proper initialization handling
|
||||||
|
- [ ] Migration time < 5s per 1000 records (planned)
|
||||||
|
- [ ] Storage overhead < 10% (planned)
|
||||||
|
- [ ] Memory usage < 50MB (planned)
|
||||||
|
|
||||||
|
### 2. Reliability
|
||||||
|
- [x] Basic data integrity
|
||||||
|
- [x] Operation queuing
|
||||||
|
- [ ] Automatic recovery (planned)
|
||||||
|
- [ ] Backup verification (planned)
|
||||||
|
- [ ] Transaction atomicity (planned)
|
||||||
|
- [ ] Data consistency (planned)
|
||||||
|
|
||||||
|
### 3. Security
|
||||||
|
- [x] Basic data integrity
|
||||||
|
- [ ] AES-256 encryption (planned)
|
||||||
|
- [ ] Secure key storage (planned)
|
||||||
|
- [ ] Access control (planned)
|
||||||
|
- [ ] Audit logging (planned)
|
||||||
|
|
||||||
|
### 4. User Experience
|
||||||
|
- [x] Basic database operations
|
||||||
|
- [ ] Smooth migration (planned)
|
||||||
|
- [ ] Clear error messages (planned)
|
||||||
|
- [ ] Progress indicators (planned)
|
||||||
|
- [ ] Recovery options (planned)
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@
|
|||||||
504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };
|
504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };
|
||||||
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
|
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
|
||||||
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
|
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
|
||||||
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; };
|
97EF2DC6FD76C3643D680B8D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 90DCAFB4D8948F7A50C13800 /* Pods_App.framework */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
@@ -27,9 +27,9 @@
|
|||||||
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
|
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
|
||||||
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
90DCAFB4D8948F7A50C13800 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
|
E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
|
EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -37,17 +37,17 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */,
|
97EF2DC6FD76C3643D680B8D /* Pods_App.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = {
|
4B546315E668C7A13939F417 /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */,
|
90DCAFB4D8948F7A50C13800 /* Pods_App.framework */,
|
||||||
);
|
);
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -57,8 +57,8 @@
|
|||||||
children = (
|
children = (
|
||||||
504EC3061FED79650016851F /* App */,
|
504EC3061FED79650016851F /* App */,
|
||||||
504EC3051FED79650016851F /* Products */,
|
504EC3051FED79650016851F /* Products */,
|
||||||
7F8756D8B27F46E3366F6CEA /* Pods */,
|
BA325FFCDCE8D334E5C7AEBE /* Pods */,
|
||||||
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
|
4B546315E668C7A13939F417 /* Frameworks */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -85,13 +85,13 @@
|
|||||||
path = App;
|
path = App;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
7F8756D8B27F46E3366F6CEA /* Pods */ = {
|
BA325FFCDCE8D334E5C7AEBE /* Pods */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */,
|
EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */,
|
||||||
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */,
|
E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */,
|
||||||
);
|
);
|
||||||
name = Pods;
|
path = Pods;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
@@ -101,12 +101,13 @@
|
|||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */;
|
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */,
|
92977BEA1068CC097A57FC77 /* [CP] Check Pods Manifest.lock */,
|
||||||
504EC3001FED79650016851F /* Sources */,
|
504EC3001FED79650016851F /* Sources */,
|
||||||
504EC3011FED79650016851F /* Frameworks */,
|
504EC3011FED79650016851F /* Frameworks */,
|
||||||
504EC3021FED79650016851F /* Resources */,
|
504EC3021FED79650016851F /* Resources */,
|
||||||
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */,
|
|
||||||
012076E8FFE4BF260A79B034 /* Fix Privacy Manifest */,
|
012076E8FFE4BF260A79B034 /* Fix Privacy Manifest */,
|
||||||
|
3525031ED1C96EF4CF6E9959 /* [CP] Embed Pods Frameworks */,
|
||||||
|
96A7EF592DF3366D00084D51 /* Fix Privacy Manifest */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@@ -186,28 +187,10 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
shellScript = "\"${PROJECT_DIR}/app_privacy_manifest_fixer/fixer.sh\" ";
|
shellScript = "\"${PROJECT_DIR}/app_privacy_manifest_fixer/fixer.sh\" \n";
|
||||||
showEnvVarsInLog = 0;
|
showEnvVarsInLog = 0;
|
||||||
};
|
};
|
||||||
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */ = {
|
3525031ED1C96EF4CF6E9959 /* [CP] Embed Pods Frameworks */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
inputPaths = (
|
|
||||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
|
||||||
"${PODS_ROOT}/Manifest.lock",
|
|
||||||
);
|
|
||||||
name = "[CP] Check Pods Manifest.lock";
|
|
||||||
outputPaths = (
|
|
||||||
"$(DERIVED_FILE_DIR)/Pods-App-checkManifestLockResult.txt",
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
shellPath = /bin/sh;
|
|
||||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
|
||||||
showEnvVarsInLog = 0;
|
|
||||||
};
|
|
||||||
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */ = {
|
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
@@ -222,6 +205,47 @@
|
|||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-frameworks.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-frameworks.sh\"\n";
|
||||||
showEnvVarsInLog = 0;
|
showEnvVarsInLog = 0;
|
||||||
};
|
};
|
||||||
|
92977BEA1068CC097A57FC77 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-App-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
|
96A7EF592DF3366D00084D51 /* Fix Privacy Manifest */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
name = "Fix Privacy Manifest";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "$PROJECT_DIR/app_privacy_manifest_fixer/fixer.sh\n";
|
||||||
|
};
|
||||||
/* End PBXShellScriptBuildPhase section */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
@@ -375,11 +399,11 @@
|
|||||||
};
|
};
|
||||||
504EC3171FED79650016851F /* Debug */ = {
|
504EC3171FED79650016851F /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */;
|
baseConfigurationReference = EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 18;
|
CURRENT_PROJECT_VERSION = 25;
|
||||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||||
ENABLE_APP_SANDBOX = NO;
|
ENABLE_APP_SANDBOX = NO;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
@@ -389,7 +413,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.4.7;
|
MARKETING_VERSION = 0.5.0;
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@@ -402,11 +426,11 @@
|
|||||||
};
|
};
|
||||||
504EC3181FED79650016851F /* Release */ = {
|
504EC3181FED79650016851F /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */;
|
baseConfigurationReference = E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 18;
|
CURRENT_PROJECT_VERSION = 25;
|
||||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||||
ENABLE_APP_SANDBOX = NO;
|
ENABLE_APP_SANDBOX = NO;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
@@ -416,7 +440,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.4.7;
|
MARKETING_VERSION = 0.5.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import Capacitor
|
import Capacitor
|
||||||
|
import CapacitorCommunitySqlite
|
||||||
|
|
||||||
@UIApplicationMain
|
@UIApplicationMain
|
||||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
@@ -7,6 +8,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
var window: UIWindow?
|
var window: UIWindow?
|
||||||
|
|
||||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||||
|
// Initialize SQLite
|
||||||
|
//let sqlite = SQLite()
|
||||||
|
//sqlite.initialize()
|
||||||
|
|
||||||
// Override point for customization after application launch.
|
// Override point for customization after application launch.
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ install! 'cocoapods', :disable_input_output_paths => true
|
|||||||
def capacitor_pods
|
def capacitor_pods
|
||||||
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
||||||
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
||||||
|
pod 'CapacitorCommunitySqlite', :path => '../../node_modules/@capacitor-community/sqlite'
|
||||||
pod 'CapacitorMlkitBarcodeScanning', :path => '../../node_modules/@capacitor-mlkit/barcode-scanning'
|
pod 'CapacitorMlkitBarcodeScanning', :path => '../../node_modules/@capacitor-mlkit/barcode-scanning'
|
||||||
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
||||||
pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera'
|
pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera'
|
||||||
@@ -26,4 +27,9 @@ end
|
|||||||
|
|
||||||
post_install do |installer|
|
post_install do |installer|
|
||||||
assertDeploymentTarget(installer)
|
assertDeploymentTarget(installer)
|
||||||
|
installer.pods_project.targets.each do |target|
|
||||||
|
target.build_configurations.each do |config|
|
||||||
|
config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ PODS:
|
|||||||
- Capacitor
|
- Capacitor
|
||||||
- CapacitorCamera (6.1.2):
|
- CapacitorCamera (6.1.2):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
|
- CapacitorCommunitySqlite (6.0.2):
|
||||||
|
- Capacitor
|
||||||
|
- SQLCipher
|
||||||
|
- ZIPFoundation
|
||||||
- CapacitorCordova (6.2.1)
|
- CapacitorCordova (6.2.1)
|
||||||
- CapacitorFilesystem (6.0.3):
|
- CapacitorFilesystem (6.0.3):
|
||||||
- Capacitor
|
- Capacitor
|
||||||
@@ -73,11 +77,18 @@ PODS:
|
|||||||
- nanopb/decode (2.30910.0)
|
- nanopb/decode (2.30910.0)
|
||||||
- nanopb/encode (2.30910.0)
|
- nanopb/encode (2.30910.0)
|
||||||
- PromisesObjC (2.4.0)
|
- PromisesObjC (2.4.0)
|
||||||
|
- SQLCipher (4.9.0):
|
||||||
|
- SQLCipher/standard (= 4.9.0)
|
||||||
|
- SQLCipher/common (4.9.0)
|
||||||
|
- SQLCipher/standard (4.9.0):
|
||||||
|
- SQLCipher/common
|
||||||
|
- ZIPFoundation (0.9.19)
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- "Capacitor (from `../../node_modules/@capacitor/ios`)"
|
- "Capacitor (from `../../node_modules/@capacitor/ios`)"
|
||||||
- "CapacitorApp (from `../../node_modules/@capacitor/app`)"
|
- "CapacitorApp (from `../../node_modules/@capacitor/app`)"
|
||||||
- "CapacitorCamera (from `../../node_modules/@capacitor/camera`)"
|
- "CapacitorCamera (from `../../node_modules/@capacitor/camera`)"
|
||||||
|
- "CapacitorCommunitySqlite (from `../../node_modules/@capacitor-community/sqlite`)"
|
||||||
- "CapacitorCordova (from `../../node_modules/@capacitor/ios`)"
|
- "CapacitorCordova (from `../../node_modules/@capacitor/ios`)"
|
||||||
- "CapacitorFilesystem (from `../../node_modules/@capacitor/filesystem`)"
|
- "CapacitorFilesystem (from `../../node_modules/@capacitor/filesystem`)"
|
||||||
- "CapacitorMlkitBarcodeScanning (from `../../node_modules/@capacitor-mlkit/barcode-scanning`)"
|
- "CapacitorMlkitBarcodeScanning (from `../../node_modules/@capacitor-mlkit/barcode-scanning`)"
|
||||||
@@ -98,6 +109,8 @@ SPEC REPOS:
|
|||||||
- MLKitVision
|
- MLKitVision
|
||||||
- nanopb
|
- nanopb
|
||||||
- PromisesObjC
|
- PromisesObjC
|
||||||
|
- SQLCipher
|
||||||
|
- ZIPFoundation
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
Capacitor:
|
Capacitor:
|
||||||
@@ -106,6 +119,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../../node_modules/@capacitor/app"
|
:path: "../../node_modules/@capacitor/app"
|
||||||
CapacitorCamera:
|
CapacitorCamera:
|
||||||
:path: "../../node_modules/@capacitor/camera"
|
:path: "../../node_modules/@capacitor/camera"
|
||||||
|
CapacitorCommunitySqlite:
|
||||||
|
:path: "../../node_modules/@capacitor-community/sqlite"
|
||||||
CapacitorCordova:
|
CapacitorCordova:
|
||||||
:path: "../../node_modules/@capacitor/ios"
|
:path: "../../node_modules/@capacitor/ios"
|
||||||
CapacitorFilesystem:
|
CapacitorFilesystem:
|
||||||
@@ -121,6 +136,7 @@ SPEC CHECKSUMS:
|
|||||||
Capacitor: c95400d761e376be9da6be5a05f226c0e865cebf
|
Capacitor: c95400d761e376be9da6be5a05f226c0e865cebf
|
||||||
CapacitorApp: e1e6b7d05e444d593ca16fd6d76f2b7c48b5aea7
|
CapacitorApp: e1e6b7d05e444d593ca16fd6d76f2b7c48b5aea7
|
||||||
CapacitorCamera: 9bc7b005d0e6f1d5f525b8137045b60cffffce79
|
CapacitorCamera: 9bc7b005d0e6f1d5f525b8137045b60cffffce79
|
||||||
|
CapacitorCommunitySqlite: 0299d20f4b00c2e6aa485a1d8932656753937b9b
|
||||||
CapacitorCordova: 8d93e14982f440181be7304aa9559ca631d77fff
|
CapacitorCordova: 8d93e14982f440181be7304aa9559ca631d77fff
|
||||||
CapacitorFilesystem: 59270a63c60836248812671aa3b15df673fbaf74
|
CapacitorFilesystem: 59270a63c60836248812671aa3b15df673fbaf74
|
||||||
CapacitorMlkitBarcodeScanning: 7652be9c7922f39203a361de735d340ae37e134e
|
CapacitorMlkitBarcodeScanning: 7652be9c7922f39203a361de735d340ae37e134e
|
||||||
@@ -138,7 +154,9 @@ SPEC CHECKSUMS:
|
|||||||
MLKitVision: 90922bca854014a856f8b649d1f1f04f63fd9c79
|
MLKitVision: 90922bca854014a856f8b649d1f1f04f63fd9c79
|
||||||
nanopb: 438bc412db1928dac798aa6fd75726007be04262
|
nanopb: 438bc412db1928dac798aa6fd75726007be04262
|
||||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||||
|
SQLCipher: 31878d8ebd27e5c96db0b7cb695c96e9f8ad77da
|
||||||
|
ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c
|
||||||
|
|
||||||
PODFILE CHECKSUM: 7e7e09e6937de7f015393aecf2cf7823645689b3
|
PODFILE CHECKSUM: f987510f7383b04a1b09ea8472bdadcd88b6c924
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
29
main.js
29
main.js
@@ -1,29 +0,0 @@
|
|||||||
const { app, BrowserWindow } = require('electron');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
function createWindow() {
|
|
||||||
const win = new BrowserWindow({
|
|
||||||
width: 1200,
|
|
||||||
height: 800,
|
|
||||||
webPreferences: {
|
|
||||||
nodeIntegration: true,
|
|
||||||
contextIsolation: false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
win.loadFile(path.join(__dirname, 'dist-electron/www/index.html'));
|
|
||||||
}
|
|
||||||
|
|
||||||
app.whenReady().then(createWindow);
|
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
|
||||||
if (process.platform !== 'darwin') {
|
|
||||||
app.quit();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on('activate', () => {
|
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
|
||||||
createWindow();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
4749
package-lock.json
generated
4749
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "timesafari",
|
"name": "timesafari",
|
||||||
"version": "0.4.6",
|
"version": "0.4.8",
|
||||||
"description": "Time Safari Application",
|
"description": "Time Safari Application",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Time Safari Team"
|
"name": "Time Safari Team"
|
||||||
@@ -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",
|
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js && node scripts/copy-wasm.js",
|
||||||
"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",
|
||||||
@@ -46,6 +46,7 @@
|
|||||||
"electron:build-mac-universal": "npm run build:electron-prod && electron-builder --mac --universal"
|
"electron:build-mac-universal": "npm run build:electron-prod && electron-builder --mac --universal"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@capacitor-community/sqlite": "6.0.2",
|
||||||
"@capacitor-mlkit/barcode-scanning": "^6.0.0",
|
"@capacitor-mlkit/barcode-scanning": "^6.0.0",
|
||||||
"@capacitor/android": "^6.2.0",
|
"@capacitor/android": "^6.2.0",
|
||||||
"@capacitor/app": "^6.0.0",
|
"@capacitor/app": "^6.0.0",
|
||||||
@@ -63,6 +64,7 @@
|
|||||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
||||||
"@fortawesome/vue-fontawesome": "^3.0.6",
|
"@fortawesome/vue-fontawesome": "^3.0.6",
|
||||||
|
"@jlongster/sql.js": "^1.6.7",
|
||||||
"@peculiar/asn1-ecc": "^2.3.8",
|
"@peculiar/asn1-ecc": "^2.3.8",
|
||||||
"@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",
|
||||||
@@ -81,6 +83,7 @@
|
|||||||
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
"@vue-leaflet/vue-leaflet": "^0.10.1",
|
||||||
"@vueuse/core": "^12.3.0",
|
"@vueuse/core": "^12.3.0",
|
||||||
"@zxing/text-encoding": "^0.9.0",
|
"@zxing/text-encoding": "^0.9.0",
|
||||||
|
"absurd-sql": "^0.0.54",
|
||||||
"asn1-ber": "^1.2.2",
|
"asn1-ber": "^1.2.2",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
"cbor-x": "^1.5.9",
|
"cbor-x": "^1.5.9",
|
||||||
@@ -144,7 +147,9 @@
|
|||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
"@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",
|
||||||
"concurrently": "^8.2.2",
|
"concurrently": "^8.2.2",
|
||||||
|
"crypto-browserify": "^3.12.1",
|
||||||
"electron": "^33.2.1",
|
"electron": "^33.2.1",
|
||||||
"electron-builder": "^25.1.8",
|
"electron-builder": "^25.1.8",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
@@ -155,13 +160,14 @@
|
|||||||
"markdownlint": "^0.37.4",
|
"markdownlint": "^0.37.4",
|
||||||
"markdownlint-cli": "^0.44.0",
|
"markdownlint-cli": "^0.44.0",
|
||||||
"npm-check-updates": "^17.1.13",
|
"npm-check-updates": "^17.1.13",
|
||||||
|
"path-browserify": "^1.0.1",
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
"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": "^0.19.8"
|
"vite-plugin-pwa": "^1.0.0"
|
||||||
},
|
},
|
||||||
"main": "./dist-electron/main.js",
|
"main": "./dist-electron/main.js",
|
||||||
"build": {
|
"build": {
|
||||||
@@ -176,7 +182,7 @@
|
|||||||
],
|
],
|
||||||
"extraResources": [
|
"extraResources": [
|
||||||
{
|
{
|
||||||
"from": "dist",
|
"from": "dist-electron/www",
|
||||||
"to": "www"
|
"to": "www"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -2,5 +2,6 @@ dependencies:
|
|||||||
- gradle
|
- gradle
|
||||||
- java
|
- java
|
||||||
- pod
|
- pod
|
||||||
|
- rubygems.org
|
||||||
|
|
||||||
# other dependencies are discovered via package.json & requirements.txt & Gemfile (I'm guessing).
|
# other dependencies are discovered via package.json & requirements.txt & Gemfile (I'm guessing).
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
console.log('Starting electron build process...');
|
console.log('Starting electron build process...');
|
||||||
|
|
||||||
// Copy web files
|
// Define paths
|
||||||
const webDistPath = path.join(__dirname, '..', 'dist');
|
|
||||||
const electronDistPath = path.join(__dirname, '..', 'dist-electron');
|
const electronDistPath = path.join(__dirname, '..', 'dist-electron');
|
||||||
const wwwPath = path.join(electronDistPath, 'www');
|
const wwwPath = path.join(electronDistPath, 'www');
|
||||||
|
|
||||||
@@ -13,231 +12,154 @@ if (!fs.existsSync(wwwPath)) {
|
|||||||
fs.mkdirSync(wwwPath, { recursive: true });
|
fs.mkdirSync(wwwPath, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy web files to www directory
|
// Create a platform-specific index.html for Electron
|
||||||
fs.cpSync(webDistPath, wwwPath, { recursive: true });
|
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>`;
|
||||||
|
|
||||||
// Fix asset paths in index.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');
|
const indexPath = path.join(wwwPath, 'index.html');
|
||||||
let indexContent = fs.readFileSync(indexPath, 'utf8');
|
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
|
// Fix asset paths
|
||||||
indexContent = indexContent
|
console.log('Fixing asset paths in index.html...');
|
||||||
|
let modifiedIndexContent = fs.readFileSync(indexPath, 'utf8');
|
||||||
|
modifiedIndexContent = modifiedIndexContent
|
||||||
.replace(/\/assets\//g, './assets/')
|
.replace(/\/assets\//g, './assets/')
|
||||||
.replace(/href="\//g, 'href="./')
|
.replace(/href="\//g, 'href="./')
|
||||||
.replace(/src="\//g, 'src="./');
|
.replace(/src="\//g, 'src="./');
|
||||||
|
|
||||||
fs.writeFileSync(indexPath, indexContent);
|
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
|
// Check for remaining /assets/ paths
|
||||||
console.log('After path fixing, checking for remaining /assets/ paths:', indexContent.includes('/assets/'));
|
console.log('After path fixing, checking for remaining /assets/ paths:', finalContent.includes('/assets/'));
|
||||||
console.log('Sample of fixed content:', indexContent.substring(0, 500));
|
console.log('Sample of fixed content:', finalContent.substring(0, 500));
|
||||||
|
|
||||||
console.log('Copied and fixed web files in:', wwwPath);
|
console.log('Copied and fixed web files in:', wwwPath);
|
||||||
|
|
||||||
// Copy main process files
|
// Copy main process files
|
||||||
console.log('Copying main process files...');
|
console.log('Copying main process files...');
|
||||||
|
|
||||||
// Create the main process file with inlined logger
|
// Copy the main process file instead of creating a template
|
||||||
const mainContent = `const { app, BrowserWindow } = require("electron");
|
const mainSrcPath = path.join(__dirname, '..', 'dist-electron', 'main.js');
|
||||||
const path = require("path");
|
const mainDestPath = path.join(electronDistPath, 'main.js');
|
||||||
const fs = require("fs");
|
|
||||||
|
|
||||||
// Inline logger implementation
|
if (fs.existsSync(mainSrcPath)) {
|
||||||
const logger = {
|
fs.copyFileSync(mainSrcPath, mainDestPath);
|
||||||
log: (...args) => console.log(...args),
|
console.log('Copied main process file successfully');
|
||||||
error: (...args) => console.error(...args),
|
} else {
|
||||||
info: (...args) => console.info(...args),
|
console.error('Main process file not found at:', mainSrcPath);
|
||||||
warn: (...args) => console.warn(...args),
|
process.exit(1);
|
||||||
debug: (...args) => console.debug(...args),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if running in dev mode
|
|
||||||
const isDev = process.argv.includes("--inspect");
|
|
||||||
|
|
||||||
function createWindow() {
|
|
||||||
// Add before createWindow function
|
|
||||||
const preloadPath = path.join(__dirname, "preload.js");
|
|
||||||
logger.log("Checking preload path:", preloadPath);
|
|
||||||
logger.log("Preload exists:", fs.existsSync(preloadPath));
|
|
||||||
|
|
||||||
// Create the browser window.
|
|
||||||
const mainWindow = new BrowserWindow({
|
|
||||||
width: 1200,
|
|
||||||
height: 800,
|
|
||||||
webPreferences: {
|
|
||||||
nodeIntegration: false,
|
|
||||||
contextIsolation: true,
|
|
||||||
webSecurity: true,
|
|
||||||
allowRunningInsecureContent: false,
|
|
||||||
preload: path.join(__dirname, "preload.js"),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Always open DevTools for now
|
|
||||||
mainWindow.webContents.openDevTools();
|
|
||||||
|
|
||||||
// Intercept requests to fix asset paths
|
|
||||||
mainWindow.webContents.session.webRequest.onBeforeRequest(
|
|
||||||
{
|
|
||||||
urls: [
|
|
||||||
"file://*/*/assets/*",
|
|
||||||
"file://*/assets/*",
|
|
||||||
"file:///assets/*", // Catch absolute paths
|
|
||||||
"<all_urls>", // Catch all URLs as a fallback
|
|
||||||
],
|
|
||||||
},
|
|
||||||
(details, callback) => {
|
|
||||||
let url = details.url;
|
|
||||||
|
|
||||||
// Handle paths that don't start with file://
|
|
||||||
if (!url.startsWith("file://") && url.includes("/assets/")) {
|
|
||||||
url = \`file://\${path.join(__dirname, "www", url)}\`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle absolute paths starting with /assets/
|
|
||||||
if (url.includes("/assets/") && !url.includes("/www/assets/")) {
|
|
||||||
const baseDir = url.includes("dist-electron")
|
|
||||||
? url.substring(
|
|
||||||
0,
|
|
||||||
url.indexOf("/dist-electron") + "/dist-electron".length,
|
|
||||||
)
|
|
||||||
: \`file://\${__dirname}\`;
|
|
||||||
const assetPath = url.split("/assets/")[1];
|
|
||||||
const newUrl = \`\${baseDir}/www/assets/\${assetPath}\`;
|
|
||||||
callback({ redirectURL: newUrl });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
callback({}); // No redirect for other URLs
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isDev) {
|
|
||||||
// Debug info
|
|
||||||
logger.log("Debug Info:");
|
|
||||||
logger.log("Running in dev mode:", isDev);
|
|
||||||
logger.log("App is packaged:", app.isPackaged);
|
|
||||||
logger.log("Process resource path:", process.resourcesPath);
|
|
||||||
logger.log("App path:", app.getAppPath());
|
|
||||||
logger.log("__dirname:", __dirname);
|
|
||||||
logger.log("process.cwd():", process.cwd());
|
|
||||||
}
|
|
||||||
|
|
||||||
const indexPath = path.join(__dirname, "www", "index.html");
|
|
||||||
|
|
||||||
if (isDev) {
|
|
||||||
logger.log("Loading index from:", indexPath);
|
|
||||||
logger.log("www path:", path.join(__dirname, "www"));
|
|
||||||
logger.log("www assets path:", path.join(__dirname, "www", "assets"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fs.existsSync(indexPath)) {
|
|
||||||
logger.error(\`Index file not found at: \${indexPath}\`);
|
|
||||||
throw new Error("Index file not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add CSP headers to allow API connections, Google Fonts, and zxing-wasm
|
|
||||||
mainWindow.webContents.session.webRequest.onHeadersReceived(
|
|
||||||
(details, callback) => {
|
|
||||||
callback({
|
|
||||||
responseHeaders: {
|
|
||||||
...details.responseHeaders,
|
|
||||||
"Content-Security-Policy": [
|
|
||||||
"default-src 'self';" +
|
|
||||||
"connect-src 'self' https://api.endorser.ch https://*.timesafari.app https://*.jsdelivr.net;" +
|
|
||||||
"img-src 'self' data: https: blob:;" +
|
|
||||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://*.jsdelivr.net;" +
|
|
||||||
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;" +
|
|
||||||
"font-src 'self' data: https://fonts.gstatic.com;" +
|
|
||||||
"style-src-elem 'self' 'unsafe-inline' https://fonts.googleapis.com;" +
|
|
||||||
"worker-src 'self' blob:;",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Load the index.html
|
|
||||||
mainWindow
|
|
||||||
.loadFile(indexPath)
|
|
||||||
.then(() => {
|
|
||||||
logger.log("Successfully loaded index.html");
|
|
||||||
if (isDev) {
|
|
||||||
mainWindow.webContents.openDevTools();
|
|
||||||
logger.log("DevTools opened - running in dev mode");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
logger.error("Failed to load index.html:", err);
|
|
||||||
logger.error("Attempted path:", indexPath);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen for console messages from the renderer
|
|
||||||
mainWindow.webContents.on("console-message", (_event, _level, message) => {
|
|
||||||
logger.log("Renderer Console:", message);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add right after creating the BrowserWindow
|
|
||||||
mainWindow.webContents.on(
|
|
||||||
"did-fail-load",
|
|
||||||
(_event, errorCode, errorDescription) => {
|
|
||||||
logger.error("Page failed to load:", errorCode, errorDescription);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
mainWindow.webContents.on("preload-error", (_event, preloadPath, error) => {
|
|
||||||
logger.error("Preload script error:", preloadPath, error);
|
|
||||||
});
|
|
||||||
|
|
||||||
mainWindow.webContents.on(
|
|
||||||
"console-message",
|
|
||||||
(_event, _level, message, line, sourceId) => {
|
|
||||||
logger.log("Renderer Console:", line, sourceId, message);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Enable remote debugging when in dev mode
|
|
||||||
if (isDev) {
|
|
||||||
mainWindow.webContents.openDevTools();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle app ready
|
console.log('Electron build process completed successfully');
|
||||||
app.whenReady().then(createWindow);
|
|
||||||
|
|
||||||
// Handle all windows closed
|
|
||||||
app.on("window-all-closed", () => {
|
|
||||||
if (process.platform !== "darwin") {
|
|
||||||
app.quit();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on("activate", () => {
|
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
|
||||||
createWindow();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle any errors
|
|
||||||
process.on("uncaughtException", (error) => {
|
|
||||||
logger.error("Uncaught Exception:", error);
|
|
||||||
});
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Write the main process file
|
|
||||||
const mainDest = path.join(electronDistPath, 'main.js');
|
|
||||||
fs.writeFileSync(mainDest, mainContent);
|
|
||||||
|
|
||||||
// Copy preload script if it exists
|
|
||||||
const preloadSrc = path.join(__dirname, '..', 'src', 'electron', 'preload.js');
|
|
||||||
const preloadDest = path.join(electronDistPath, 'preload.js');
|
|
||||||
if (fs.existsSync(preloadSrc)) {
|
|
||||||
console.log(`Copying ${preloadSrc} to ${preloadDest}`);
|
|
||||||
fs.copyFileSync(preloadSrc, preloadDest);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify build structure
|
|
||||||
console.log('\nVerifying build structure:');
|
|
||||||
console.log('Files in dist-electron:', fs.readdirSync(electronDistPath));
|
|
||||||
|
|
||||||
console.log('Build completed successfully!');
|
|
||||||
15
scripts/copy-wasm.js
Normal file
15
scripts/copy-wasm.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Create public/wasm directory if it doesn't exist
|
||||||
|
const wasmDir = path.join(__dirname, '../public/wasm');
|
||||||
|
if (!fs.existsSync(wasmDir)) {
|
||||||
|
fs.mkdirSync(wasmDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the WASM file from node_modules to public/wasm
|
||||||
|
const sourceFile = path.join(__dirname, '../node_modules/@jlongster/sql.js/dist/sql-wasm.wasm');
|
||||||
|
const targetFile = path.join(wasmDir, 'sql-wasm.wasm');
|
||||||
|
|
||||||
|
fs.copyFileSync(sourceFile, targetFile);
|
||||||
|
console.log('WASM file copied successfully!');
|
||||||
25
src/App.vue
25
src/App.vue
@@ -4,7 +4,7 @@
|
|||||||
<!-- Messages in the upper-right - https://github.com/emmanuelsw/notiwind -->
|
<!-- Messages in the upper-right - https://github.com/emmanuelsw/notiwind -->
|
||||||
<NotificationGroup group="alert">
|
<NotificationGroup group="alert">
|
||||||
<div
|
<div
|
||||||
class="fixed top-[calc(env(safe-area-inset-top)+1rem)] right-4 left-4 sm:left-auto sm:w-full sm:max-w-sm flex flex-col items-start justify-end"
|
class="fixed z-[90] top-[max(1rem,env(safe-area-inset-top))] right-4 left-4 sm:left-auto sm:w-full sm:max-w-sm flex flex-col items-start justify-end"
|
||||||
>
|
>
|
||||||
<Notification
|
<Notification
|
||||||
v-slot="{ notifications, close }"
|
v-slot="{ notifications, close }"
|
||||||
@@ -330,8 +330,11 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Vue, Component } from "vue-facing-decorator";
|
import { Vue, Component } from "vue-facing-decorator";
|
||||||
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from "./db/index";
|
|
||||||
import { NotificationIface } from "./constants/app";
|
import { NotificationIface, USE_DEXIE_DB } from "./constants/app";
|
||||||
|
import * as databaseUtil from "./db/databaseUtil";
|
||||||
|
import { retrieveSettingsForActiveAccount } from "./db/index";
|
||||||
|
import { logConsoleAndDb } from "./db/databaseUtil";
|
||||||
import { logger } from "./utils/logger";
|
import { logger } from "./utils/logger";
|
||||||
|
|
||||||
interface Settings {
|
interface Settings {
|
||||||
@@ -396,7 +399,11 @@ export default class App extends Vue {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
logger.log("Retrieving settings for the active account...");
|
logger.log("Retrieving settings for the active account...");
|
||||||
const settings: Settings = await retrieveSettingsForActiveAccount();
|
let settings: Settings =
|
||||||
|
await databaseUtil.retrieveSettingsForActiveAccount();
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
settings = await retrieveSettingsForActiveAccount();
|
||||||
|
}
|
||||||
logger.log("Retrieved settings:", settings);
|
logger.log("Retrieved settings:", settings);
|
||||||
|
|
||||||
const notifyingNewActivity = !!settings?.notifyingNewActivityTime;
|
const notifyingNewActivity = !!settings?.notifyingNewActivityTime;
|
||||||
@@ -541,13 +548,13 @@ export default class App extends Vue {
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
#Content {
|
#Content {
|
||||||
padding-left: 1.5rem;
|
padding-left: max(1.5rem, env(safe-area-inset-left));
|
||||||
padding-right: 1.5rem;
|
padding-right: max(1.5rem, env(safe-area-inset-right));
|
||||||
padding-top: calc(env(safe-area-inset-top) + 1.5rem);
|
padding-top: max(1.5rem, env(safe-area-inset-top));
|
||||||
padding-bottom: calc(env(safe-area-inset-bottom) + 1.5rem);
|
padding-bottom: max(1.5rem, env(safe-area-inset-bottom));
|
||||||
}
|
}
|
||||||
|
|
||||||
#QuickNav ~ #Content {
|
#QuickNav ~ #Content {
|
||||||
padding-bottom: calc(env(safe-area-inset-bottom) + 6rem);
|
padding-bottom: calc(env(safe-area-inset-bottom) + 6.333rem);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -40,10 +40,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3
|
<h3 v-if="record.issuer.known" class="font-semibold leading-tight">
|
||||||
v-if="record.issuer.known"
|
|
||||||
class="font-semibold leading-tight"
|
|
||||||
>
|
|
||||||
{{ record.issuer.displayName }}
|
{{ record.issuer.displayName }}
|
||||||
</h3>
|
</h3>
|
||||||
<p class="ms-auto text-xs text-slate-500 italic">
|
<p class="ms-auto text-xs text-slate-500 italic">
|
||||||
@@ -52,7 +49,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a class="cursor-pointer" @click="$emit('loadClaim', record.jwtId)">
|
<a
|
||||||
|
class="cursor-pointer"
|
||||||
|
data-testid="circle-info-link"
|
||||||
|
@click="$emit('loadClaim', record.jwtId)"
|
||||||
|
>
|
||||||
<font-awesome icon="circle-info" class="fa-fw text-slate-500" />
|
<font-awesome icon="circle-info" class="fa-fw text-slate-500" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -61,7 +62,7 @@
|
|||||||
<!-- Record Image -->
|
<!-- Record Image -->
|
||||||
<div
|
<div
|
||||||
v-if="record.image"
|
v-if="record.image"
|
||||||
class="bg-cover mb-4 -mt-3 sm:-mt-4 -mx-3 sm:-mx-4"
|
class="bg-cover mb-2 -mt-3 sm:-mt-4 -mx-3 sm:-mx-4"
|
||||||
:style="`background-image: url(${record.image});`"
|
:style="`background-image: url(${record.image});`"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@@ -77,8 +78,15 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<p class="font-medium">
|
||||||
|
<a class="cursor-pointer" @click="$emit('loadClaim', record.jwtId)">
|
||||||
|
{{ description }}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="relative flex justify-between gap-4 max-w-[40rem] mx-auto mb-3"
|
class="relative flex justify-between gap-4 max-w-[40rem] mx-auto mt-4"
|
||||||
>
|
>
|
||||||
<!-- Source -->
|
<!-- Source -->
|
||||||
<div
|
<div
|
||||||
@@ -90,7 +98,9 @@
|
|||||||
<div v-if="record.providerPlanName">
|
<div v-if="record.providerPlanName">
|
||||||
<router-link
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
path: '/project/' + encodeURIComponent(record.providerPlanHandleId || ''),
|
path:
|
||||||
|
'/project/' +
|
||||||
|
encodeURIComponent(record.providerPlanHandleId || ''),
|
||||||
}"
|
}"
|
||||||
title="View project details"
|
title="View project details"
|
||||||
>
|
>
|
||||||
@@ -118,17 +128,17 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
<font-awesome
|
<font-awesome
|
||||||
v-else
|
v-else
|
||||||
@click="notifyHiddenPerson"
|
|
||||||
icon="eye-slash"
|
icon="eye-slash"
|
||||||
class="text-slate-300 !size-[3rem] sm:!size-[4rem]"
|
class="text-slate-300 !size-[3rem] sm:!size-[4rem]"
|
||||||
|
@click="notifyHiddenPerson"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- Unknown Person -->
|
<!-- Unknown Person -->
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<font-awesome
|
<font-awesome
|
||||||
@click="notifyUnknownPerson"
|
|
||||||
icon="person-circle-question"
|
icon="person-circle-question"
|
||||||
class="text-slate-300 text-[3rem] sm:text-[4rem]"
|
class="text-slate-300 text-[3rem] sm:text-[4rem]"
|
||||||
|
@click="notifyUnknownPerson"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -149,7 +159,9 @@
|
|||||||
<div
|
<div
|
||||||
class="absolute inset-x-[7rem] sm:inset-x-[12rem] mx-2 top-1/2 -translate-y-1/2"
|
class="absolute inset-x-[7rem] sm:inset-x-[12rem] mx-2 top-1/2 -translate-y-1/2"
|
||||||
>
|
>
|
||||||
<div class="text-sm text-center leading-none font-semibold pe-2 sm:pe-4">
|
<div
|
||||||
|
class="text-sm text-center leading-none font-semibold pe-2 sm:pe-4"
|
||||||
|
>
|
||||||
{{ fetchAmount }}
|
{{ fetchAmount }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -174,7 +186,9 @@
|
|||||||
<div v-if="record.recipientProjectName">
|
<div v-if="record.recipientProjectName">
|
||||||
<router-link
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
path: '/project/' + encodeURIComponent(record.fulfillsPlanHandleId || ''),
|
path:
|
||||||
|
'/project/' +
|
||||||
|
encodeURIComponent(record.fulfillsPlanHandleId || ''),
|
||||||
}"
|
}"
|
||||||
title="View project details"
|
title="View project details"
|
||||||
>
|
>
|
||||||
@@ -202,17 +216,17 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
<font-awesome
|
<font-awesome
|
||||||
v-else
|
v-else
|
||||||
@click="notifyHiddenPerson"
|
|
||||||
icon="eye-slash"
|
icon="eye-slash"
|
||||||
class="text-slate-300 !size-[3rem] sm:!size-[4rem]"
|
class="text-slate-300 !size-[3rem] sm:!size-[4rem]"
|
||||||
|
@click="notifyHiddenPerson"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- Unknown Person -->
|
<!-- Unknown Person -->
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<font-awesome
|
<font-awesome
|
||||||
@click="notifyUnknownPerson"
|
|
||||||
icon="person-circle-question"
|
icon="person-circle-question"
|
||||||
class="text-slate-300 text-[3rem] sm:text-[4rem]"
|
class="text-slate-300 text-[3rem] sm:text-[4rem]"
|
||||||
|
@click="notifyUnknownPerson"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -229,13 +243,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Description -->
|
|
||||||
<p class="font-medium">
|
|
||||||
<a class="cursor-pointer" @click="$emit('loadClaim', record.jwtId)">
|
|
||||||
{{ description }}
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
@@ -273,7 +280,7 @@ export default class ActivityListItem extends Vue {
|
|||||||
title: "Person Outside Your Network",
|
title: "Person Outside Your Network",
|
||||||
text: "This person is not visible to you.",
|
text: "This person is not visible to you.",
|
||||||
},
|
},
|
||||||
3000
|
3000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,7 +292,7 @@ export default class ActivityListItem extends Vue {
|
|||||||
title: "Unidentified Person",
|
title: "Unidentified Person",
|
||||||
text: "Nobody specific was recognized.",
|
text: "Nobody specific was recognized.",
|
||||||
},
|
},
|
||||||
3000
|
3000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,7 +316,7 @@ export default class ActivityListItem extends Vue {
|
|||||||
const claim =
|
const claim =
|
||||||
(this.record.fullClaim as unknown).claim || this.record.fullClaim;
|
(this.record.fullClaim as unknown).claim || this.record.fullClaim;
|
||||||
|
|
||||||
return `${claim.description}`;
|
return `${claim?.description || ""}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private displayAmount(code: string, amt: number) {
|
private displayAmount(code: string, amt: number) {
|
||||||
|
|||||||
@@ -24,9 +24,7 @@ backup and database export, with platform-specific download instructions. * *
|
|||||||
class="block w-full text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
|
class="block w-full text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
|
||||||
@click="exportDatabase()"
|
@click="exportDatabase()"
|
||||||
>
|
>
|
||||||
Download Settings & Contacts
|
Download Contacts
|
||||||
<br />
|
|
||||||
(excluding Identifier Data)
|
|
||||||
</button>
|
</button>
|
||||||
<a
|
<a
|
||||||
ref="downloadLink"
|
ref="downloadLink"
|
||||||
@@ -62,14 +60,18 @@ backup and database export, with platform-specific download instructions. * *
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue } from "vue-facing-decorator";
|
import { Component, Prop, Vue } from "vue-facing-decorator";
|
||||||
import { NotificationIface } from "../constants/app";
|
import { AppString, NotificationIface } from "../constants/app";
|
||||||
import { db } from "../db/index";
|
|
||||||
|
import { Contact } from "../db/tables/contacts";
|
||||||
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
|
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
||||||
import {
|
import {
|
||||||
PlatformService,
|
PlatformService,
|
||||||
PlatformCapabilities,
|
PlatformCapabilities,
|
||||||
} from "../services/PlatformService";
|
} from "../services/PlatformService";
|
||||||
|
import { contactsToExportJson } from "../libs/util";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @vue-component
|
* @vue-component
|
||||||
@@ -131,21 +133,25 @@ export default class DataExportSection extends Vue {
|
|||||||
*/
|
*/
|
||||||
public async exportDatabase() {
|
public async exportDatabase() {
|
||||||
try {
|
try {
|
||||||
const blob = await db.export({
|
let allContacts: Contact[] = [];
|
||||||
prettyJson: true,
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
transform: (table, value, key) => {
|
const result = await platformService.dbQuery(`SELECT * FROM contacts`);
|
||||||
if (table === "contacts") {
|
if (result) {
|
||||||
// Dexie inserts a number 0 when some are undefined, so we need to totally remove them.
|
allContacts = databaseUtil.mapQueryResultToValues(
|
||||||
Object.keys(value).forEach((prop) => {
|
result,
|
||||||
if (value[prop] === undefined) {
|
) as unknown as Contact[];
|
||||||
delete value[prop];
|
}
|
||||||
}
|
// if (USE_DEXIE_DB) {
|
||||||
});
|
// await db.open();
|
||||||
}
|
// allContacts = await db.contacts.toArray();
|
||||||
return { value, key };
|
// }
|
||||||
},
|
|
||||||
});
|
// Convert contacts to export format
|
||||||
const fileName = `${db.name}-backup.json`;
|
const exportData = contactsToExportJson(allContacts);
|
||||||
|
const jsonStr = JSON.stringify(exportData, null, 2);
|
||||||
|
const blob = new Blob([jsonStr], { type: "application/json" });
|
||||||
|
|
||||||
|
const fileName = `${AppString.APP_NAME_NO_SPACES}-backup-contacts.json`;
|
||||||
|
|
||||||
if (this.platformCapabilities.hasFileDownload) {
|
if (this.platformCapabilities.hasFileDownload) {
|
||||||
// Web platform: Use download link
|
// Web platform: Use download link
|
||||||
@@ -157,8 +163,9 @@ export default class DataExportSection extends Vue {
|
|||||||
setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000);
|
setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000);
|
||||||
} else if (this.platformCapabilities.hasFileSystem) {
|
} else if (this.platformCapabilities.hasFileSystem) {
|
||||||
// Native platform: Write to app directory
|
// Native platform: Write to app directory
|
||||||
const content = await blob.text();
|
await this.platformService.writeAndShareFile(fileName, jsonStr);
|
||||||
await this.platformService.writeAndShareFile(fileName, content);
|
} else {
|
||||||
|
throw new Error("This platform does not support file downloads.");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$notify(
|
this.$notify(
|
||||||
@@ -167,10 +174,10 @@ export default class DataExportSection extends Vue {
|
|||||||
type: "success",
|
type: "success",
|
||||||
title: "Export Successful",
|
title: "Export Successful",
|
||||||
text: this.platformCapabilities.hasFileDownload
|
text: this.platformCapabilities.hasFileDownload
|
||||||
? "See your downloads directory for the backup. It is in the Dexie format."
|
? "See your downloads directory for the backup."
|
||||||
: "You should have been prompted to save your backup file.",
|
: "The backup file has been saved.",
|
||||||
},
|
},
|
||||||
-1,
|
3000,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Export Error:", error);
|
logger.error("Export Error:", error);
|
||||||
|
|||||||
@@ -99,8 +99,12 @@ import {
|
|||||||
LTileLayer,
|
LTileLayer,
|
||||||
} from "@vue-leaflet/vue-leaflet";
|
} from "@vue-leaflet/vue-leaflet";
|
||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
|
|
||||||
|
import { USE_DEXIE_DB } from "@/constants/app";
|
||||||
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
||||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||||
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
@@ -122,7 +126,10 @@ export default class FeedFilters extends Vue {
|
|||||||
async open(onCloseIfChanged: () => void) {
|
async open(onCloseIfChanged: () => void) {
|
||||||
this.onCloseIfChanged = onCloseIfChanged;
|
this.onCloseIfChanged = onCloseIfChanged;
|
||||||
|
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
settings = await retrieveSettingsForActiveAccount();
|
||||||
|
}
|
||||||
this.hasVisibleDid = !!settings.filterFeedByVisible;
|
this.hasVisibleDid = !!settings.filterFeedByVisible;
|
||||||
this.isNearby = !!settings.filterFeedByNearby;
|
this.isNearby = !!settings.filterFeedByNearby;
|
||||||
if (settings.searchBoxes && settings.searchBoxes.length > 0) {
|
if (settings.searchBoxes && settings.searchBoxes.length > 0) {
|
||||||
@@ -144,9 +151,17 @@ export default class FeedFilters extends Vue {
|
|||||||
async toggleNearby() {
|
async toggleNearby() {
|
||||||
this.settingChanged = true;
|
this.settingChanged = true;
|
||||||
this.isNearby = !this.isNearby;
|
this.isNearby = !this.isNearby;
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
filterFeedByNearby: this.isNearby,
|
await platformService.dbExec(
|
||||||
});
|
`UPDATE settings SET filterFeedByNearby = ? WHERE id = ?`,
|
||||||
|
[this.isNearby, MASTER_SETTINGS_KEY],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
filterFeedByNearby: this.isNearby,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearAll() {
|
async clearAll() {
|
||||||
@@ -154,10 +169,18 @@ export default class FeedFilters extends Vue {
|
|||||||
this.settingChanged = true;
|
this.settingChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
filterFeedByNearby: false,
|
await platformService.dbExec(
|
||||||
filterFeedByVisible: false,
|
`UPDATE settings SET filterFeedByNearby = ? AND filterFeedByVisible = ? WHERE id = ?`,
|
||||||
});
|
[false, false, MASTER_SETTINGS_KEY],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
filterFeedByNearby: false,
|
||||||
|
filterFeedByVisible: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.hasVisibleDid = false;
|
this.hasVisibleDid = false;
|
||||||
this.isNearby = false;
|
this.isNearby = false;
|
||||||
@@ -168,10 +191,18 @@ export default class FeedFilters extends Vue {
|
|||||||
this.settingChanged = true;
|
this.settingChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
filterFeedByNearby: true,
|
await platformService.dbExec(
|
||||||
filterFeedByVisible: true,
|
`UPDATE settings SET filterFeedByNearby = ? AND filterFeedByVisible = ? WHERE id = ?`,
|
||||||
});
|
[true, true, MASTER_SETTINGS_KEY],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
filterFeedByNearby: true,
|
||||||
|
filterFeedByVisible: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.hasVisibleDid = true;
|
this.hasVisibleDid = true;
|
||||||
this.isNearby = true;
|
this.isNearby = true;
|
||||||
|
|||||||
@@ -89,7 +89,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||||
|
|
||||||
import { NotificationIface } from "../constants/app";
|
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||||
import {
|
import {
|
||||||
createAndSubmitGive,
|
createAndSubmitGive,
|
||||||
didInfo,
|
didInfo,
|
||||||
@@ -98,8 +98,10 @@ import {
|
|||||||
import * as libsUtil from "../libs/util";
|
import * as libsUtil from "../libs/util";
|
||||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from "../db/tables/contacts";
|
||||||
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
import { retrieveAccountDids } from "../libs/util";
|
import { retrieveAccountDids } from "../libs/util";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class GiftedDialog extends Vue {
|
export default class GiftedDialog extends Vue {
|
||||||
@@ -144,11 +146,23 @@ export default class GiftedDialog extends Vue {
|
|||||||
this.offerId = offerId || "";
|
this.offerId = offerId || "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
settings = await retrieveSettingsForActiveAccount();
|
||||||
|
}
|
||||||
this.apiServer = settings.apiServer || "";
|
this.apiServer = settings.apiServer || "";
|
||||||
this.activeDid = settings.activeDid || "";
|
this.activeDid = settings.activeDid || "";
|
||||||
|
|
||||||
this.allContacts = await db.contacts.toArray();
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
|
const result = await platformService.dbQuery(`SELECT * FROM contacts`);
|
||||||
|
if (result) {
|
||||||
|
this.allContacts = databaseUtil.mapQueryResultToValues(
|
||||||
|
result,
|
||||||
|
) as unknown as Contact[];
|
||||||
|
}
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
this.allContacts = await db.contacts.toArray();
|
||||||
|
}
|
||||||
|
|
||||||
this.allMyDids = await retrieveAccountDids();
|
this.allMyDids = await retrieveAccountDids();
|
||||||
|
|
||||||
@@ -306,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(
|
||||||
@@ -356,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
|
||||||
|
|||||||
@@ -74,10 +74,12 @@
|
|||||||
import { Vue, Component } from "vue-facing-decorator";
|
import { Vue, Component } from "vue-facing-decorator";
|
||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
|
|
||||||
import { AppString, NotificationIface } from "../constants/app";
|
import { AppString, NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||||
import { db } from "../db/index";
|
import { db } from "../db/index";
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from "../db/tables/contacts";
|
||||||
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
import { GiverReceiverInputInfo } from "../libs/util";
|
import { GiverReceiverInputInfo } from "../libs/util";
|
||||||
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class GivenPrompts extends Vue {
|
export default class GivenPrompts extends Vue {
|
||||||
@@ -127,8 +129,16 @@ export default class GivenPrompts extends Vue {
|
|||||||
this.visible = true;
|
this.visible = true;
|
||||||
this.callbackOnFullGiftInfo = callbackOnFullGiftInfo;
|
this.callbackOnFullGiftInfo = callbackOnFullGiftInfo;
|
||||||
|
|
||||||
await db.open();
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
this.numContacts = await db.contacts.count();
|
const result = await platformService.dbQuery(
|
||||||
|
"SELECT COUNT(*) FROM contacts",
|
||||||
|
);
|
||||||
|
if (result) {
|
||||||
|
this.numContacts = result.values[0][0] as number;
|
||||||
|
}
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
this.numContacts = await db.contacts.count();
|
||||||
|
}
|
||||||
this.shownContactDbIndices = new Array<boolean>(this.numContacts); // all undefined to start
|
this.shownContactDbIndices = new Array<boolean>(this.numContacts); // all undefined to start
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,10 +239,22 @@ export default class GivenPrompts extends Vue {
|
|||||||
this.nextIdeaPastContacts();
|
this.nextIdeaPastContacts();
|
||||||
} else {
|
} else {
|
||||||
// get the contact at that offset
|
// get the contact at that offset
|
||||||
await db.open();
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
this.currentContact = await db.contacts
|
const result = await platformService.dbQuery(
|
||||||
.offset(someContactDbIndex)
|
"SELECT * FROM contacts LIMIT 1 OFFSET ?",
|
||||||
.first();
|
[someContactDbIndex],
|
||||||
|
);
|
||||||
|
if (result) {
|
||||||
|
this.currentContact = databaseUtil.mapQueryResultToValues(result)[
|
||||||
|
someContactDbIndex
|
||||||
|
] as unknown as Contact;
|
||||||
|
}
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
await db.open();
|
||||||
|
this.currentContact = await db.contacts
|
||||||
|
.offset(someContactDbIndex)
|
||||||
|
.first();
|
||||||
|
}
|
||||||
this.shownContactDbIndices[someContactDbIndex] = true;
|
this.shownContactDbIndices[someContactDbIndex] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,11 +48,7 @@
|
|||||||
<span>
|
<span>
|
||||||
{{ didInfo(visDid) }}
|
{{ didInfo(visDid) }}
|
||||||
<span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)">
|
<span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)">
|
||||||
<a
|
<a :href="`/did/${visDid}`" class="text-blue-500">
|
||||||
:href="`/did/${visDid}`"
|
|
||||||
target="_blank"
|
|
||||||
class="text-blue-500"
|
|
||||||
>
|
|
||||||
<font-awesome
|
<font-awesome
|
||||||
icon="arrow-up-right-from-square"
|
icon="arrow-up-right-from-square"
|
||||||
class="fa-fw"
|
class="fa-fw"
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
<div class="text-lg text-center font-bold relative">
|
<div class="text-lg text-center font-bold relative">
|
||||||
<h1 id="ViewHeading" class="text-center font-bold">
|
<h1 id="ViewHeading" class="text-center font-bold">
|
||||||
<span v-if="uploading">Uploading Image…</span>
|
<span v-if="uploading">Uploading Image…</span>
|
||||||
<span v-else-if="blob">{{ crop ? 'Crop Image' : 'Preview Image' }}</span>
|
<span v-else-if="blob">{{
|
||||||
|
crop ? "Crop Image" : "Preview Image"
|
||||||
|
}}</span>
|
||||||
<span v-else-if="showCameraPreview">Upload Image</span>
|
<span v-else-if="showCameraPreview">Upload Image</span>
|
||||||
<span v-else>Add Photo</span>
|
<span v-else>Add Photo</span>
|
||||||
</h1>
|
</h1>
|
||||||
@@ -119,7 +121,9 @@
|
|||||||
playsinline
|
playsinline
|
||||||
muted
|
muted
|
||||||
></video>
|
></video>
|
||||||
<div class="absolute bottom-4 inset-x-0 flex items-center justify-center gap-4">
|
<div
|
||||||
|
class="absolute bottom-4 inset-x-0 flex items-center justify-center gap-4"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
class="bg-white text-slate-800 p-3 rounded-full text-2xl leading-none"
|
class="bg-white text-slate-800 p-3 rounded-full text-2xl leading-none"
|
||||||
@click="capturePhoto"
|
@click="capturePhoto"
|
||||||
@@ -238,12 +242,12 @@
|
|||||||
<p class="mb-2">
|
<p class="mb-2">
|
||||||
Before you can upload a photo, a friend needs to register you.
|
Before you can upload a photo, a friend needs to register you.
|
||||||
</p>
|
</p>
|
||||||
<router-link
|
<button
|
||||||
:to="{ name: 'contact-qr' }"
|
|
||||||
class="inline-block text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
class="inline-block text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
|
||||||
|
@click="handleQRCodeClick"
|
||||||
>
|
>
|
||||||
Share Your Info
|
Share Your Info
|
||||||
</router-link>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -256,11 +260,17 @@ import axios from "axios";
|
|||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import VuePictureCropper, { cropper } from "vue-picture-cropper";
|
import VuePictureCropper, { cropper } from "vue-picture-cropper";
|
||||||
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
|
import { Capacitor } from "@capacitor/core";
|
||||||
|
import {
|
||||||
|
DEFAULT_IMAGE_API_SERVER,
|
||||||
|
NotificationIface,
|
||||||
|
USE_DEXIE_DB,
|
||||||
|
} from "../constants/app";
|
||||||
import { retrieveSettingsForActiveAccount } from "../db/index";
|
import { retrieveSettingsForActiveAccount } from "../db/index";
|
||||||
import { accessToken } from "../libs/crypto";
|
import { accessToken } from "../libs/crypto";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
||||||
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
|
|
||||||
const inputImageFileNameRef = ref<Blob>();
|
const inputImageFileNameRef = ref<Blob>();
|
||||||
|
|
||||||
@@ -273,9 +283,9 @@ const inputImageFileNameRef = ref<Blob>();
|
|||||||
},
|
},
|
||||||
defaultCameraMode: {
|
defaultCameraMode: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'environment',
|
default: "environment",
|
||||||
validator: (value: string) => ['environment', 'user'].includes(value)
|
validator: (value: string) => ["environment", "user"].includes(value),
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class ImageMethodDialog extends Vue {
|
export default class ImageMethodDialog extends Vue {
|
||||||
@@ -318,7 +328,7 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
private cameraStream: MediaStream | null = null;
|
private cameraStream: MediaStream | null = null;
|
||||||
|
|
||||||
/** Current camera facing mode */
|
/** Current camera facing mode */
|
||||||
private currentFacingMode: 'environment' | 'user' = 'environment';
|
private currentFacingMode: "environment" | "user" = "environment";
|
||||||
|
|
||||||
private platformService = PlatformServiceFactory.getInstance();
|
private platformService = PlatformServiceFactory.getInstance();
|
||||||
URL = window.URL || window.webkitURL;
|
URL = window.URL || window.webkitURL;
|
||||||
@@ -350,9 +360,11 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
* @throws {Error} When settings retrieval fails
|
* @throws {Error} When settings retrieval fails
|
||||||
*/
|
*/
|
||||||
async mounted() {
|
async mounted() {
|
||||||
logger.log("ImageMethodDialog mounted");
|
|
||||||
try {
|
try {
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
settings = await retrieveSettingsForActiveAccount();
|
||||||
|
}
|
||||||
this.activeDid = settings.activeDid || "";
|
this.activeDid = settings.activeDid || "";
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
logger.error("Error retrieving settings from database:", error);
|
logger.error("Error retrieving settings from database:", error);
|
||||||
@@ -384,7 +396,7 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
this.crop = !!crop;
|
this.crop = !!crop;
|
||||||
this.imageCallback = setImageFn;
|
this.imageCallback = setImageFn;
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
this.currentFacingMode = this.defaultCameraMode as 'environment' | 'user';
|
this.currentFacingMode = this.defaultCameraMode as "environment" | "user";
|
||||||
|
|
||||||
// Start camera preview immediately
|
// Start camera preview immediately
|
||||||
logger.debug("Starting camera preview from open()");
|
logger.debug("Starting camera preview from open()");
|
||||||
@@ -458,7 +470,10 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
logger.debug("Current showCameraPreview state:", this.showCameraPreview);
|
logger.debug("Current showCameraPreview state:", this.showCameraPreview);
|
||||||
logger.debug("Platform capabilities:", this.platformCapabilities);
|
logger.debug("Platform capabilities:", this.platformCapabilities);
|
||||||
logger.debug("MediaDevices available:", !!navigator.mediaDevices);
|
logger.debug("MediaDevices available:", !!navigator.mediaDevices);
|
||||||
logger.debug("getUserMedia available:", !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia));
|
logger.debug(
|
||||||
|
"getUserMedia available:",
|
||||||
|
!!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia),
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.cameraState = "initializing";
|
this.cameraState = "initializing";
|
||||||
@@ -485,13 +500,16 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
videoElement.srcObject = stream;
|
videoElement.srcObject = stream;
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
videoElement.onloadedmetadata = () => {
|
videoElement.onloadedmetadata = () => {
|
||||||
videoElement.play().then(() => {
|
videoElement
|
||||||
logger.debug("Video element started playing");
|
.play()
|
||||||
resolve(true);
|
.then(() => {
|
||||||
}).catch(error => {
|
logger.debug("Video element started playing");
|
||||||
logger.error("Error playing video:", error);
|
resolve(true);
|
||||||
throw error;
|
})
|
||||||
});
|
.catch((error) => {
|
||||||
|
logger.error("Error playing video:", error);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -503,17 +521,16 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
let errorMessage =
|
let errorMessage =
|
||||||
error instanceof Error ? error.message : "Failed to access camera";
|
error instanceof Error ? error.message : "Failed to access camera";
|
||||||
if (
|
if (
|
||||||
error instanceof Error && (
|
error instanceof Error &&
|
||||||
error.name === "NotReadableError" ||
|
(error.name === "NotReadableError" || error.name === "TrackStartError")
|
||||||
error.name === "TrackStartError"
|
) {
|
||||||
)) {
|
|
||||||
errorMessage =
|
errorMessage =
|
||||||
"Camera is in use by another application. Please close any other apps or browser tabs using the camera and try again.";
|
"Camera is in use by another application. Please close any other apps or browser tabs using the camera and try again.";
|
||||||
} else if (
|
} else if (
|
||||||
error instanceof Error && (
|
error instanceof Error &&
|
||||||
error.name === "NotAllowedError" ||
|
(error.name === "NotAllowedError" ||
|
||||||
error.name === "PermissionDeniedError"
|
error.name === "PermissionDeniedError")
|
||||||
)) {
|
) {
|
||||||
errorMessage =
|
errorMessage =
|
||||||
"Camera access was denied. Please allow camera access in your browser settings.";
|
"Camera access was denied. Please allow camera access in your browser settings.";
|
||||||
}
|
}
|
||||||
@@ -583,11 +600,12 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
|
|
||||||
async rotateCamera() {
|
async rotateCamera() {
|
||||||
// Toggle between front and back cameras
|
// Toggle between front and back cameras
|
||||||
this.currentFacingMode = this.currentFacingMode === 'environment' ? 'user' : 'environment';
|
this.currentFacingMode =
|
||||||
|
this.currentFacingMode === "environment" ? "user" : "environment";
|
||||||
|
|
||||||
// Stop current stream
|
// Stop current stream
|
||||||
if (this.cameraStream) {
|
if (this.cameraStream) {
|
||||||
this.cameraStream.getTracks().forEach(track => track.stop());
|
this.cameraStream.getTracks().forEach((track) => track.stop());
|
||||||
this.cameraStream = null;
|
this.cameraStream = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -692,6 +710,14 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
toggleDiagnostics() {
|
toggleDiagnostics() {
|
||||||
this.showDiagnostics = !this.showDiagnostics;
|
this.showDiagnostics = !this.showDiagnostics;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handleQRCodeClick() {
|
||||||
|
if (Capacitor.isNativePlatform()) {
|
||||||
|
this.$router.push({ name: "contact-qr-scan-full" });
|
||||||
|
} else {
|
||||||
|
this.$router.push({ name: "contact-qr" });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -172,8 +172,10 @@ import {
|
|||||||
} from "../libs/endorserServer";
|
} from "../libs/endorserServer";
|
||||||
import { decryptMessage } from "../libs/crypto";
|
import { decryptMessage } from "../libs/crypto";
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from "../db/tables/contacts";
|
||||||
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
import * as libsUtil from "../libs/util";
|
import * as libsUtil from "../libs/util";
|
||||||
import { NotificationIface } from "../constants/app";
|
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||||
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||||
|
|
||||||
interface Member {
|
interface Member {
|
||||||
admitted: boolean;
|
admitted: boolean;
|
||||||
@@ -209,7 +211,10 @@ export default class MembersList extends Vue {
|
|||||||
contacts: Array<Contact> = [];
|
contacts: Array<Contact> = [];
|
||||||
|
|
||||||
async created() {
|
async created() {
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
settings = await retrieveSettingsForActiveAccount();
|
||||||
|
}
|
||||||
this.activeDid = settings.activeDid || "";
|
this.activeDid = settings.activeDid || "";
|
||||||
this.apiServer = settings.apiServer || "";
|
this.apiServer = settings.apiServer || "";
|
||||||
this.firstName = settings.firstName || "";
|
this.firstName = settings.firstName || "";
|
||||||
@@ -355,7 +360,16 @@ export default class MembersList extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async loadContacts() {
|
async loadContacts() {
|
||||||
this.contacts = await db.contacts.toArray();
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
|
const result = await platformService.dbQuery("SELECT * FROM contacts");
|
||||||
|
if (result) {
|
||||||
|
this.contacts = databaseUtil.mapQueryResultToValues(
|
||||||
|
result,
|
||||||
|
) as unknown as Contact[];
|
||||||
|
}
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
this.contacts = await db.contacts.toArray();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getContactFor(did: string): Contact | undefined {
|
getContactFor(did: string): Contact | undefined {
|
||||||
@@ -439,7 +453,14 @@ export default class MembersList extends Vue {
|
|||||||
if (result.success) {
|
if (result.success) {
|
||||||
decrMember.isRegistered = true;
|
decrMember.isRegistered = true;
|
||||||
if (oldContact) {
|
if (oldContact) {
|
||||||
await db.contacts.update(decrMember.did, { registered: true });
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
|
await platformService.dbExec(
|
||||||
|
"UPDATE contacts SET registered = ? WHERE did = ?",
|
||||||
|
[true, decrMember.did],
|
||||||
|
);
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
await db.contacts.update(decrMember.did, { registered: true });
|
||||||
|
}
|
||||||
oldContact.registered = true;
|
oldContact.registered = true;
|
||||||
}
|
}
|
||||||
this.$notify(
|
this.$notify(
|
||||||
@@ -492,7 +513,14 @@ export default class MembersList extends Vue {
|
|||||||
name: member.name,
|
name: member.name,
|
||||||
};
|
};
|
||||||
|
|
||||||
await db.contacts.add(newContact);
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
|
await platformService.dbExec(
|
||||||
|
"INSERT INTO contacts (did, name) VALUES (?, ?)",
|
||||||
|
[member.did, member.name],
|
||||||
|
);
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
await db.contacts.add(newContact);
|
||||||
|
}
|
||||||
this.contacts.push(newContact);
|
this.contacts.push(newContact);
|
||||||
|
|
||||||
this.$notify(
|
this.$notify(
|
||||||
|
|||||||
@@ -82,12 +82,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||||
|
|
||||||
import { NotificationIface } from "../constants/app";
|
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||||
import {
|
import {
|
||||||
createAndSubmitOffer,
|
createAndSubmitOffer,
|
||||||
serverMessageForUser,
|
serverMessageForUser,
|
||||||
} from "../libs/endorserServer";
|
} from "../libs/endorserServer";
|
||||||
import * as libsUtil from "../libs/util";
|
import * as libsUtil from "../libs/util";
|
||||||
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
import { retrieveSettingsForActiveAccount } from "../db/index";
|
import { retrieveSettingsForActiveAccount } from "../db/index";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
|
|
||||||
@@ -116,7 +117,10 @@ export default class OfferDialog extends Vue {
|
|||||||
this.recipientDid = recipientDid;
|
this.recipientDid = recipientDid;
|
||||||
this.recipientName = recipientName;
|
this.recipientName = recipientName;
|
||||||
|
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
settings = await retrieveSettingsForActiveAccount();
|
||||||
|
}
|
||||||
this.apiServer = settings.apiServer || "";
|
this.apiServer = settings.apiServer || "";
|
||||||
this.activeDid = settings.activeDid || "";
|
this.activeDid = settings.activeDid || "";
|
||||||
|
|
||||||
@@ -245,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(
|
||||||
@@ -292,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
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<h1 class="text-xl font-bold text-center mb-4 relative">
|
<h1 class="text-xl font-bold text-center mb-4 relative">
|
||||||
Welcome to Time Safari
|
Welcome to Time Safari
|
||||||
<br />
|
<br />
|
||||||
- Showcasing Gratitude & Magnifying Time
|
- Showcase Impact & Magnify Time
|
||||||
<div
|
<div
|
||||||
class="text-lg text-center leading-none absolute right-0 -top-1"
|
class="text-lg text-center leading-none absolute right-0 -top-1"
|
||||||
@click="onClickClose(true)"
|
@click="onClickClose(true)"
|
||||||
@@ -14,6 +14,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
The feed underneath this pop-up shows the latest contributions, some from
|
||||||
|
people and some from projects.
|
||||||
|
|
||||||
<p v-if="isRegistered" class="mt-4">
|
<p v-if="isRegistered" class="mt-4">
|
||||||
You can now log things that you've seen:
|
You can now log things that you've seen:
|
||||||
<span v-if="numContacts > 0">
|
<span v-if="numContacts > 0">
|
||||||
@@ -23,14 +26,10 @@
|
|||||||
<span class="bg-green-600 text-white rounded-full">
|
<span class="bg-green-600 text-white rounded-full">
|
||||||
<font-awesome icon="plus" class="fa-fw" />
|
<font-awesome icon="plus" class="fa-fw" />
|
||||||
</span>
|
</span>
|
||||||
button to express your appreciation for... whatever -- maybe thanks for
|
button to express your appreciation for... whatever.
|
||||||
showing you all these fascinating stories of
|
|
||||||
<em>gratitude</em>.
|
|
||||||
</p>
|
</p>
|
||||||
<p v-else class="mt-4">
|
<p class="mt-4">
|
||||||
The feed underneath this pop-up shows the latest gifts that others have
|
Once someone registers you, you can log your appreciation, too.
|
||||||
recognized. Once someone registers you, you can log your appreciation,
|
|
||||||
too.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="mt-4">
|
<p class="mt-4">
|
||||||
@@ -201,13 +200,16 @@
|
|||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
|
|
||||||
import { NotificationIface } from "../constants/app";
|
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||||
import {
|
import {
|
||||||
db,
|
db,
|
||||||
retrieveSettingsForActiveAccount,
|
retrieveSettingsForActiveAccount,
|
||||||
updateAccountSettings,
|
updateAccountSettings,
|
||||||
} from "../db/index";
|
} from "../db/index";
|
||||||
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
import { OnboardPage } from "../libs/util";
|
import { OnboardPage } from "../libs/util";
|
||||||
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||||
|
import { Contact } from "@/db/tables/contacts";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
computed: {
|
computed: {
|
||||||
@@ -222,7 +224,7 @@ export default class OnboardingDialog extends Vue {
|
|||||||
$router!: Router;
|
$router!: Router;
|
||||||
|
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
firstContactName = null;
|
firstContactName = "";
|
||||||
givenName = "";
|
givenName = "";
|
||||||
isRegistered = false;
|
isRegistered = false;
|
||||||
numContacts = 0;
|
numContacts = 0;
|
||||||
@@ -231,29 +233,54 @@ export default class OnboardingDialog extends Vue {
|
|||||||
|
|
||||||
async open(page: OnboardPage) {
|
async open(page: OnboardPage) {
|
||||||
this.page = page;
|
this.page = page;
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
settings = await retrieveSettingsForActiveAccount();
|
||||||
|
}
|
||||||
this.activeDid = settings.activeDid || "";
|
this.activeDid = settings.activeDid || "";
|
||||||
this.isRegistered = !!settings.isRegistered;
|
this.isRegistered = !!settings.isRegistered;
|
||||||
const contacts = await db.contacts.toArray();
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
this.numContacts = contacts.length;
|
const dbContacts = await platformService.dbQuery("SELECT * FROM contacts");
|
||||||
if (this.numContacts > 0) {
|
if (dbContacts) {
|
||||||
this.firstContactName = contacts[0].name;
|
this.numContacts = dbContacts.values.length;
|
||||||
|
const firstContact = dbContacts.values[0];
|
||||||
|
const fullContact = databaseUtil.mapColumnsToValues(dbContacts.columns, [
|
||||||
|
firstContact,
|
||||||
|
]) as unknown as Contact;
|
||||||
|
this.firstContactName = fullContact.name || "";
|
||||||
|
}
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
const contacts = await db.contacts.toArray();
|
||||||
|
this.numContacts = contacts.length;
|
||||||
|
if (this.numContacts > 0) {
|
||||||
|
this.firstContactName = contacts[0].name || "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
if (this.page === OnboardPage.Create) {
|
if (this.page === OnboardPage.Create) {
|
||||||
// we'll assume that they've been through all the other pages
|
// we'll assume that they've been through all the other pages
|
||||||
await updateAccountSettings(this.activeDid, {
|
await databaseUtil.updateAccountSettings(this.activeDid, {
|
||||||
finishedOnboarding: true,
|
finishedOnboarding: true,
|
||||||
});
|
});
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
await updateAccountSettings(this.activeDid, {
|
||||||
|
finishedOnboarding: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClickClose(done?: boolean, goHome?: boolean) {
|
async onClickClose(done?: boolean, goHome?: boolean) {
|
||||||
this.visible = false;
|
this.visible = false;
|
||||||
if (done) {
|
if (done) {
|
||||||
await updateAccountSettings(this.activeDid, {
|
await databaseUtil.updateAccountSettings(this.activeDid, {
|
||||||
finishedOnboarding: true,
|
finishedOnboarding: true,
|
||||||
});
|
});
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
await updateAccountSettings(this.activeDid, {
|
||||||
|
finishedOnboarding: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
if (goHome) {
|
if (goHome) {
|
||||||
this.$router.push({ name: "home" });
|
this.$router.push({ name: "home" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,7 +119,12 @@ PhotoDialog.vue */
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import VuePictureCropper, { cropper } from "vue-picture-cropper";
|
import VuePictureCropper, { cropper } from "vue-picture-cropper";
|
||||||
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
|
import {
|
||||||
|
DEFAULT_IMAGE_API_SERVER,
|
||||||
|
NotificationIface,
|
||||||
|
USE_DEXIE_DB,
|
||||||
|
} from "../constants/app";
|
||||||
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
import { retrieveSettingsForActiveAccount } from "../db/index";
|
import { retrieveSettingsForActiveAccount } from "../db/index";
|
||||||
import { accessToken } from "../libs/crypto";
|
import { accessToken } from "../libs/crypto";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
@@ -173,9 +178,12 @@ export default class PhotoDialog extends Vue {
|
|||||||
* @throws {Error} When settings retrieval fails
|
* @throws {Error} When settings retrieval fails
|
||||||
*/
|
*/
|
||||||
async mounted() {
|
async mounted() {
|
||||||
logger.log("PhotoDialog mounted");
|
// logger.log("PhotoDialog mounted");
|
||||||
try {
|
try {
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
settings = await retrieveSettingsForActiveAccount();
|
||||||
|
}
|
||||||
this.activeDid = settings.activeDid || "";
|
this.activeDid = settings.activeDid || "";
|
||||||
this.isRegistered = !!settings.isRegistered;
|
this.isRegistered = !!settings.isRegistered;
|
||||||
logger.log("isRegistered:", this.isRegistered);
|
logger.log("isRegistered:", this.isRegistered);
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
<!-- eslint-disable vue/no-v-html -->
|
<!-- eslint-disable vue/no-v-html -->
|
||||||
<template>
|
<template>
|
||||||
<a
|
<a
|
||||||
v-if="linkToFull && imageUrl"
|
v-if="linkToFullImage && imageUrl"
|
||||||
:href="imageUrl"
|
:href="imageUrl"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="h-full w-full object-contain"
|
class="h-full w-full object-contain"
|
||||||
>
|
>
|
||||||
<div class="h-full w-full object-contain" v-html="generateIdenticon()" />
|
<div class="h-full w-full object-contain" v-html="generateIcon()" />
|
||||||
</a>
|
</a>
|
||||||
<div
|
<div v-else class="h-full w-full object-contain" v-html="generateIcon()" />
|
||||||
v-else
|
|
||||||
class="h-full w-full object-contain"
|
|
||||||
v-html="generateIdenticon()"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { toSvg } from "jdenticon";
|
import { toSvg } from "jdenticon";
|
||||||
@@ -35,9 +31,9 @@ export default class ProjectIcon extends Vue {
|
|||||||
@Prop entityId = "";
|
@Prop entityId = "";
|
||||||
@Prop iconSize = 0;
|
@Prop iconSize = 0;
|
||||||
@Prop imageUrl = "";
|
@Prop imageUrl = "";
|
||||||
@Prop linkToFull = false;
|
@Prop linkToFullImage = false;
|
||||||
|
|
||||||
generateIdenticon() {
|
generateIcon() {
|
||||||
if (this.imageUrl) {
|
if (this.imageUrl) {
|
||||||
return `<img src="${this.imageUrl}" class="w-full h-full object-contain" />`;
|
return `<img src="${this.imageUrl}" class="w-full h-full object-contain" />`;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -102,7 +102,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
|
||||||
import { DEFAULT_PUSH_SERVER, NotificationIface } from "../constants/app";
|
import {
|
||||||
|
DEFAULT_PUSH_SERVER,
|
||||||
|
NotificationIface,
|
||||||
|
USE_DEXIE_DB,
|
||||||
|
} from "../constants/app";
|
||||||
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
import {
|
import {
|
||||||
logConsoleAndDb,
|
logConsoleAndDb,
|
||||||
retrieveSettingsForActiveAccount,
|
retrieveSettingsForActiveAccount,
|
||||||
@@ -169,7 +174,10 @@ export default class PushNotificationPermission extends Vue {
|
|||||||
this.isVisible = true;
|
this.isVisible = true;
|
||||||
this.pushType = pushType;
|
this.pushType = pushType;
|
||||||
try {
|
try {
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
settings = await retrieveSettingsForActiveAccount();
|
||||||
|
}
|
||||||
let pushUrl = DEFAULT_PUSH_SERVER;
|
let pushUrl = DEFAULT_PUSH_SERVER;
|
||||||
if (settings?.webPushServer) {
|
if (settings?.webPushServer) {
|
||||||
pushUrl = settings.webPushServer;
|
pushUrl = settings.webPushServer;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="absolute right-5 top-[calc(env(safe-area-inset-top)+0.75rem)]">
|
<div class="absolute right-5 top-[max(0.75rem,env(safe-area-inset-top))]">
|
||||||
<span class="align-center text-red-500 mr-2">{{ message }}</span>
|
<span class="align-center text-red-500 mr-2">{{ message }}</span>
|
||||||
<span class="ml-2">
|
<span class="ml-2">
|
||||||
<router-link
|
<router-link
|
||||||
@@ -15,7 +15,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue, Prop } from "vue-facing-decorator";
|
import { Component, Vue, Prop } from "vue-facing-decorator";
|
||||||
|
|
||||||
import { AppString, NotificationIface } from "../constants/app";
|
import { AppString, NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||||
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
import { retrieveSettingsForActiveAccount } from "../db/index";
|
import { retrieveSettingsForActiveAccount } from "../db/index";
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@@ -28,7 +29,10 @@ export default class TopMessage extends Vue {
|
|||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
try {
|
try {
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
settings = await retrieveSettingsForActiveAccount();
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
settings.warnIfTestServer &&
|
settings.warnIfTestServer &&
|
||||||
settings.apiServer !== AppString.PROD_ENDORSER_API_SERVER
|
settings.apiServer !== AppString.PROD_ENDORSER_API_SERVER
|
||||||
|
|||||||
@@ -37,9 +37,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||||
|
|
||||||
import { NotificationIface } from "../constants/app";
|
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 { 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 {
|
||||||
@@ -61,15 +63,25 @@ export default class UserNameDialog extends Vue {
|
|||||||
*/
|
*/
|
||||||
async open(aCallback?: (name?: string) => void) {
|
async open(aCallback?: (name?: string) => void) {
|
||||||
this.callback = aCallback || this.callback;
|
this.callback = aCallback || this.callback;
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
settings = await retrieveSettingsForActiveAccount();
|
||||||
|
}
|
||||||
this.givenName = settings.firstName || "";
|
this.givenName = settings.firstName || "";
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async onClickSaveChanges() {
|
async onClickSaveChanges() {
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
firstName: this.givenName,
|
await platformService.dbExec(
|
||||||
});
|
"UPDATE settings SET firstName = ? WHERE id = ?",
|
||||||
|
[this.givenName, MASTER_SETTINGS_KEY],
|
||||||
|
);
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
firstName: this.givenName,
|
||||||
|
});
|
||||||
|
}
|
||||||
this.visible = false;
|
this.visible = false;
|
||||||
this.callback(this.givenName);
|
this.callback(this.givenName);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import * as THREE from "three";
|
|||||||
import { GLTFLoader } from "three/addons/loaders/GLTFLoader";
|
import { GLTFLoader } from "three/addons/loaders/GLTFLoader";
|
||||||
import * as SkeletonUtils from "three/addons/utils/SkeletonUtils";
|
import * as SkeletonUtils from "three/addons/utils/SkeletonUtils";
|
||||||
import * as TWEEN from "@tweenjs/tween.js";
|
import * as TWEEN from "@tweenjs/tween.js";
|
||||||
|
import { USE_DEXIE_DB } from "../../../../constants/app";
|
||||||
|
import * as databaseUtil from "../../../../db/databaseUtil";
|
||||||
import { retrieveSettingsForActiveAccount } from "../../../../db";
|
import { retrieveSettingsForActiveAccount } from "../../../../db";
|
||||||
import { getHeaders } from "../../../../libs/endorserServer";
|
import { getHeaders } from "../../../../libs/endorserServer";
|
||||||
import { logger } from "../../../../utils/logger";
|
import { logger } from "../../../../utils/logger";
|
||||||
@@ -14,7 +16,10 @@ export async function loadLandmarks(vue, world, scene, loop) {
|
|||||||
vue.setWorldProperty("animationDurationSeconds", ANIMATION_DURATION_SECS);
|
vue.setWorldProperty("animationDurationSeconds", ANIMATION_DURATION_SECS);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
settings = await retrieveSettingsForActiveAccount();
|
||||||
|
}
|
||||||
const activeDid = settings.activeDid || "";
|
const activeDid = settings.activeDid || "";
|
||||||
const apiServer = settings.apiServer;
|
const apiServer = settings.apiServer;
|
||||||
const headers = await getHeaders(activeDid);
|
const headers = await getHeaders(activeDid);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export enum AppString {
|
|||||||
// This is used in titles and verbiage inside the app.
|
// This is used in titles and verbiage inside the app.
|
||||||
// There is also an app name without spaces, for packaging in the package.json file used in the manifest.
|
// There is also an app name without spaces, for packaging in the package.json file used in the manifest.
|
||||||
APP_NAME = "Time Safari",
|
APP_NAME = "Time Safari",
|
||||||
|
APP_NAME_NO_SPACES = "TimeSafari",
|
||||||
|
|
||||||
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
|
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
|
||||||
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
|
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
|
||||||
@@ -32,24 +33,26 @@ export const APP_SERVER =
|
|||||||
|
|
||||||
export const DEFAULT_ENDORSER_API_SERVER =
|
export const DEFAULT_ENDORSER_API_SERVER =
|
||||||
import.meta.env.VITE_DEFAULT_ENDORSER_API_SERVER ||
|
import.meta.env.VITE_DEFAULT_ENDORSER_API_SERVER ||
|
||||||
AppString.TEST_ENDORSER_API_SERVER;
|
AppString.PROD_ENDORSER_API_SERVER;
|
||||||
|
|
||||||
export const DEFAULT_IMAGE_API_SERVER =
|
export const DEFAULT_IMAGE_API_SERVER =
|
||||||
import.meta.env.VITE_DEFAULT_IMAGE_API_SERVER ||
|
import.meta.env.VITE_DEFAULT_IMAGE_API_SERVER ||
|
||||||
AppString.TEST_IMAGE_API_SERVER;
|
AppString.PROD_IMAGE_API_SERVER;
|
||||||
|
|
||||||
export const DEFAULT_PARTNER_API_SERVER =
|
export const DEFAULT_PARTNER_API_SERVER =
|
||||||
import.meta.env.VITE_DEFAULT_PARTNER_API_SERVER ||
|
import.meta.env.VITE_DEFAULT_PARTNER_API_SERVER ||
|
||||||
AppString.TEST_PARTNER_API_SERVER;
|
AppString.PROD_PARTNER_API_SERVER;
|
||||||
|
|
||||||
export const DEFAULT_PUSH_SERVER =
|
export const DEFAULT_PUSH_SERVER =
|
||||||
window.location.protocol + "//" + window.location.host;
|
import.meta.env.VITE_DEFAULT_PUSH_SERVER || AppString.PROD_PUSH_SERVER;
|
||||||
|
|
||||||
export const IMAGE_TYPE_PROFILE = "profile";
|
export const IMAGE_TYPE_PROFILE = "profile";
|
||||||
|
|
||||||
export const PASSKEYS_ENABLED =
|
export const PASSKEYS_ENABLED =
|
||||||
!!import.meta.env.VITE_PASSKEYS_ENABLED || false;
|
!!import.meta.env.VITE_PASSKEYS_ENABLED || false;
|
||||||
|
|
||||||
|
export const USE_DEXIE_DB = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The possible values for "group" and "type" are in App.vue.
|
* The possible values for "group" and "type" are in App.vue.
|
||||||
* Some of this comes from the notiwind package, some is custom.
|
* Some of this comes from the notiwind package, some is custom.
|
||||||
|
|||||||
138
src/db-sql/migration.ts
Normal file
138
src/db-sql/migration.ts
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import migrationService from "../services/migrationService";
|
||||||
|
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
|
||||||
|
import { arrayBufferToBase64 } from "@/libs/crypto";
|
||||||
|
|
||||||
|
// Generate a random secret for the secret table
|
||||||
|
|
||||||
|
// It's not really secure to maintain the secret next to the user's data.
|
||||||
|
// However, until we have better hooks into a real wallet or reliable secure
|
||||||
|
// storage, we'll do this for user convenience. As they sign more records
|
||||||
|
// and integrate with more people, they'll value it more and want to be more
|
||||||
|
// secure, so we'll prompt them to take steps to back it up, properly encrypt,
|
||||||
|
// etc. At the beginning, we'll prompt for a password, then we'll prompt for a
|
||||||
|
// PWA so it's not in a browser... and then we hope to be integrated with a
|
||||||
|
// real wallet or something else more secure.
|
||||||
|
|
||||||
|
// One might ask: why encrypt at all? We figure a basic encryption is better
|
||||||
|
// than none. Plus, we expect to support their own password or keystore or
|
||||||
|
// external wallet as better signing options in the future, so it's gonna be
|
||||||
|
// important to have the structure where each account access might require
|
||||||
|
// user action.
|
||||||
|
|
||||||
|
// (Once upon a time we stored the secret in localStorage, but it frequently
|
||||||
|
// got erased, even though the IndexedDB still had the identity data. This
|
||||||
|
// ended up throwing lots of errors to the user... and they'd end up in a state
|
||||||
|
// where they couldn't take action because they couldn't unlock that identity.)
|
||||||
|
|
||||||
|
const randomBytes = crypto.getRandomValues(new Uint8Array(32));
|
||||||
|
const secretBase64 = arrayBufferToBase64(randomBytes);
|
||||||
|
|
||||||
|
// Each migration can include multiple SQL statements (with semicolons)
|
||||||
|
const MIGRATIONS = [
|
||||||
|
{
|
||||||
|
name: "001_initial",
|
||||||
|
// see ../db/tables files for explanations of the fields
|
||||||
|
sql: `
|
||||||
|
CREATE TABLE IF NOT EXISTS accounts (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
dateCreated TEXT NOT NULL,
|
||||||
|
derivationPath TEXT,
|
||||||
|
did TEXT NOT NULL,
|
||||||
|
identityEncrBase64 TEXT, -- encrypted & base64-encoded
|
||||||
|
mnemonicEncrBase64 TEXT, -- encrypted & base64-encoded
|
||||||
|
passkeyCredIdHex TEXT,
|
||||||
|
publicKeyHex TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_accounts_did ON accounts(did);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS secret (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
secretBase64 TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO secret (id, secretBase64) VALUES (1, '${secretBase64}');
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
accountDid TEXT,
|
||||||
|
activeDid TEXT,
|
||||||
|
apiServer TEXT,
|
||||||
|
filterFeedByNearby BOOLEAN,
|
||||||
|
filterFeedByVisible BOOLEAN,
|
||||||
|
finishedOnboarding BOOLEAN,
|
||||||
|
firstName TEXT,
|
||||||
|
hideRegisterPromptOnNewContact BOOLEAN,
|
||||||
|
isRegistered BOOLEAN,
|
||||||
|
lastName TEXT,
|
||||||
|
lastAckedOfferToUserJwtId TEXT,
|
||||||
|
lastAckedOfferToUserProjectsJwtId TEXT,
|
||||||
|
lastNotifiedClaimId TEXT,
|
||||||
|
lastViewedClaimId TEXT,
|
||||||
|
notifyingNewActivityTime TEXT,
|
||||||
|
notifyingReminderMessage TEXT,
|
||||||
|
notifyingReminderTime TEXT,
|
||||||
|
partnerApiServer TEXT,
|
||||||
|
passkeyExpirationMinutes INTEGER,
|
||||||
|
profileImageUrl TEXT,
|
||||||
|
searchBoxes TEXT, -- Stored as JSON string
|
||||||
|
showContactGivesInline BOOLEAN,
|
||||||
|
showGeneralAdvanced BOOLEAN,
|
||||||
|
showShortcutBvc BOOLEAN,
|
||||||
|
vapid TEXT,
|
||||||
|
warnIfProdServer BOOLEAN,
|
||||||
|
warnIfTestServer BOOLEAN,
|
||||||
|
webPushServer TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_settings_accountDid ON settings(accountDid);
|
||||||
|
|
||||||
|
INSERT INTO settings (id, apiServer) VALUES (1, '${DEFAULT_ENDORSER_API_SERVER}');
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS contacts (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
did TEXT NOT NULL,
|
||||||
|
name TEXT,
|
||||||
|
contactMethods TEXT, -- Stored as JSON string
|
||||||
|
nextPubKeyHashB64 TEXT,
|
||||||
|
notes TEXT,
|
||||||
|
profileImageUrl TEXT,
|
||||||
|
publicKeyBase64 TEXT,
|
||||||
|
seesMe BOOLEAN,
|
||||||
|
registered BOOLEAN
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_contacts_did ON contacts(did);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_contacts_name ON contacts(name);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS logs (
|
||||||
|
date TEXT NOT NULL,
|
||||||
|
message TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS temp (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
blobB64 TEXT
|
||||||
|
);
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param sqlExec - A function that executes a SQL statement and returns the result
|
||||||
|
* @param extractMigrationNames - A function that extracts the names (string array) from "select name from migrations"
|
||||||
|
*/
|
||||||
|
export async function runMigrations<T>(
|
||||||
|
sqlExec: (sql: string) => Promise<unknown>,
|
||||||
|
sqlQuery: (sql: string) => Promise<T>,
|
||||||
|
extractMigrationNames: (result: T) => Set<string>,
|
||||||
|
): Promise<void> {
|
||||||
|
for (const migration of MIGRATIONS) {
|
||||||
|
migrationService.registerMigration(migration);
|
||||||
|
}
|
||||||
|
await migrationService.runMigrations(
|
||||||
|
sqlExec,
|
||||||
|
sqlQuery,
|
||||||
|
extractMigrationNames,
|
||||||
|
);
|
||||||
|
}
|
||||||
314
src/db/databaseUtil.ts
Normal file
314
src/db/databaseUtil.ts
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
/**
|
||||||
|
* This file is the SQL replacement of the index.ts file in the db directory.
|
||||||
|
* That file will eventually be deleted.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||||
|
import { MASTER_SETTINGS_KEY, Settings } from "./tables/settings";
|
||||||
|
import { logger } from "@/utils/logger";
|
||||||
|
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
|
||||||
|
import { QueryExecResult } from "@/interfaces/database";
|
||||||
|
|
||||||
|
export async function updateDefaultSettings(
|
||||||
|
settingsChanges: Settings,
|
||||||
|
): Promise<boolean> {
|
||||||
|
delete settingsChanges.accountDid; // just in case
|
||||||
|
// ensure there is no "id" that would override the key
|
||||||
|
delete settingsChanges.id;
|
||||||
|
try {
|
||||||
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
|
const { sql, params } = generateUpdateStatement(
|
||||||
|
settingsChanges,
|
||||||
|
"settings",
|
||||||
|
"id = ?",
|
||||||
|
[MASTER_SETTINGS_KEY],
|
||||||
|
);
|
||||||
|
const result = await platformService.dbExec(sql, params);
|
||||||
|
return result.changes === 1;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error updating default settings:", error);
|
||||||
|
if (error instanceof Error) {
|
||||||
|
throw error; // Re-throw if it's already an Error with a message
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to update settings. We recommend you try again or restart the app.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateAccountSettings(
|
||||||
|
accountDid: string,
|
||||||
|
settingsChanges: Settings,
|
||||||
|
): Promise<boolean> {
|
||||||
|
settingsChanges.accountDid = accountDid;
|
||||||
|
delete settingsChanges.id; // key off account, not ID
|
||||||
|
|
||||||
|
const platform = PlatformServiceFactory.getInstance();
|
||||||
|
|
||||||
|
// First try to update existing record
|
||||||
|
const { sql: updateSql, params: updateParams } = generateUpdateStatement(
|
||||||
|
settingsChanges,
|
||||||
|
"settings",
|
||||||
|
"accountDid = ?",
|
||||||
|
[accountDid],
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateResult = await platform.dbExec(updateSql, updateParams);
|
||||||
|
return updateResult.changes === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_SETTINGS: Settings = {
|
||||||
|
id: MASTER_SETTINGS_KEY,
|
||||||
|
activeDid: undefined,
|
||||||
|
apiServer: DEFAULT_ENDORSER_API_SERVER,
|
||||||
|
};
|
||||||
|
|
||||||
|
// retrieves default settings
|
||||||
|
export async function retrieveSettingsForDefaultAccount(): Promise<Settings> {
|
||||||
|
const platform = PlatformServiceFactory.getInstance();
|
||||||
|
const sql = "SELECT * FROM settings WHERE id = ?";
|
||||||
|
const result = await platform.dbQuery(sql, [MASTER_SETTINGS_KEY]);
|
||||||
|
if (!result) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves settings for the active account, merging with default settings
|
||||||
|
*
|
||||||
|
* @returns Promise<Settings> Combined settings with account-specific overrides
|
||||||
|
* @throws Will log specific errors for debugging but returns default settings on failure
|
||||||
|
*/
|
||||||
|
export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
|
||||||
|
try {
|
||||||
|
// Get default settings first
|
||||||
|
const defaultSettings = await retrieveSettingsForDefaultAccount();
|
||||||
|
// If no active DID, return defaults
|
||||||
|
if (!defaultSettings.activeDid) {
|
||||||
|
logConsoleAndDb(
|
||||||
|
"[databaseUtil] No active DID found, returning default settings",
|
||||||
|
);
|
||||||
|
return defaultSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get account-specific settings
|
||||||
|
try {
|
||||||
|
const platform = PlatformServiceFactory.getInstance();
|
||||||
|
const result = await platform.dbQuery(
|
||||||
|
"SELECT * FROM settings WHERE accountDid = ?",
|
||||||
|
[defaultSettings.activeDid],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result?.values?.length) {
|
||||||
|
logConsoleAndDb(
|
||||||
|
`[databaseUtil] No account-specific settings found for ${defaultSettings.activeDid}`,
|
||||||
|
);
|
||||||
|
return defaultSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map and filter settings
|
||||||
|
const overrideSettings = mapColumnsToValues(
|
||||||
|
result.columns,
|
||||||
|
result.values,
|
||||||
|
)[0] as Settings;
|
||||||
|
const overrideSettingsFiltered = Object.fromEntries(
|
||||||
|
Object.entries(overrideSettings).filter(([_, v]) => v !== null),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Merge settings
|
||||||
|
const settings = { ...defaultSettings, ...overrideSettingsFiltered };
|
||||||
|
|
||||||
|
// Handle searchBoxes parsing
|
||||||
|
if (settings.searchBoxes) {
|
||||||
|
try {
|
||||||
|
// @ts-expect-error - the searchBoxes field is a string in the DB
|
||||||
|
settings.searchBoxes = JSON.parse(settings.searchBoxes);
|
||||||
|
} catch (error) {
|
||||||
|
logConsoleAndDb(
|
||||||
|
`[databaseUtil] Failed to parse searchBoxes for ${defaultSettings.activeDid}: ${error}`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
// Reset to empty array on parse failure
|
||||||
|
settings.searchBoxes = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings;
|
||||||
|
} catch (error) {
|
||||||
|
logConsoleAndDb(
|
||||||
|
`[databaseUtil] Failed to retrieve account settings for ${defaultSettings.activeDid}: ${error}`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
// Return defaults on error
|
||||||
|
return defaultSettings;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logConsoleAndDb(
|
||||||
|
`[databaseUtil] Failed to retrieve default settings: ${error}`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
// Return minimal default settings on complete failure
|
||||||
|
return {
|
||||||
|
id: MASTER_SETTINGS_KEY,
|
||||||
|
activeDid: undefined,
|
||||||
|
apiServer: DEFAULT_ENDORSER_API_SERVER,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastCleanupDate: string | null = null;
|
||||||
|
export let memoryLogs: string[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs a message to the database with proper handling of concurrent writes
|
||||||
|
* @param message - The message to log
|
||||||
|
* @author Matthew Raymer
|
||||||
|
*/
|
||||||
|
export async function logToDb(message: string): Promise<void> {
|
||||||
|
const platform = PlatformServiceFactory.getInstance();
|
||||||
|
const todayKey = new Date().toDateString();
|
||||||
|
const nowKey = new Date().toISOString();
|
||||||
|
|
||||||
|
try {
|
||||||
|
memoryLogs.push(`${new Date().toISOString()} ${message}`);
|
||||||
|
// Try to insert first, if it fails due to UNIQUE constraint, update instead
|
||||||
|
await platform.dbExec("INSERT INTO logs (date, message) VALUES (?, ?)", [
|
||||||
|
nowKey,
|
||||||
|
message,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 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
|
||||||
|
if (!lastCleanupDate || lastCleanupDate !== todayKey) {
|
||||||
|
const sevenDaysAgo = new Date(
|
||||||
|
new Date().getTime() - 7 * 24 * 60 * 60 * 1000,
|
||||||
|
);
|
||||||
|
memoryLogs = memoryLogs.filter(
|
||||||
|
(log) => log.split(" ")[0] > sevenDaysAgo.toDateString(),
|
||||||
|
);
|
||||||
|
await platform.dbExec("DELETE FROM logs WHERE date < ?", [
|
||||||
|
sevenDaysAgo.toDateString(),
|
||||||
|
]);
|
||||||
|
lastCleanupDate = todayKey;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Log to console as fallback
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(
|
||||||
|
"Error logging to database:",
|
||||||
|
error,
|
||||||
|
" ... for original message:",
|
||||||
|
message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// similar method is in the sw_scripts/additional-scripts.js file
|
||||||
|
export async function logConsoleAndDb(
|
||||||
|
message: string,
|
||||||
|
isError = false,
|
||||||
|
): Promise<void> {
|
||||||
|
if (isError) {
|
||||||
|
logger.error(`${new Date().toISOString()} ${message}`);
|
||||||
|
} else {
|
||||||
|
logger.log(`${new Date().toISOString()} ${message}`);
|
||||||
|
}
|
||||||
|
await logToDb(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates an SQL INSERT statement and parameters from a model object.
|
||||||
|
* @param model The model object containing fields to update
|
||||||
|
* @param tableName The name of the table to update
|
||||||
|
* @returns Object containing the SQL statement and parameters array
|
||||||
|
*/
|
||||||
|
export function generateInsertStatement(
|
||||||
|
model: Record<string, unknown>,
|
||||||
|
tableName: string,
|
||||||
|
): { sql: string; params: unknown[] } {
|
||||||
|
const columns = Object.keys(model).filter((key) => model[key] !== undefined);
|
||||||
|
const values = Object.values(model).filter((value) => value !== undefined);
|
||||||
|
const placeholders = values.map(() => "?").join(", ");
|
||||||
|
const insertSql = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${placeholders})`;
|
||||||
|
return {
|
||||||
|
sql: insertSql,
|
||||||
|
params: values,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates an SQL UPDATE statement and parameters from a model object.
|
||||||
|
* @param model The model object containing fields to update
|
||||||
|
* @param tableName The name of the table to update
|
||||||
|
* @param whereClause The WHERE clause for the update (e.g. "id = ?")
|
||||||
|
* @param whereParams Parameters for the WHERE clause
|
||||||
|
* @returns Object containing the SQL statement and parameters array
|
||||||
|
*/
|
||||||
|
export function generateUpdateStatement(
|
||||||
|
model: Record<string, unknown>,
|
||||||
|
tableName: string,
|
||||||
|
whereClause: string,
|
||||||
|
whereParams: unknown[] = [],
|
||||||
|
): { sql: string; params: unknown[] } {
|
||||||
|
// Filter out undefined/null values and create SET clause
|
||||||
|
const setClauses: string[] = [];
|
||||||
|
const params: unknown[] = [];
|
||||||
|
|
||||||
|
Object.entries(model).forEach(([key, value]) => {
|
||||||
|
setClauses.push(`${key} = ?`);
|
||||||
|
params.push(value ?? null);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (setClauses.length === 0) {
|
||||||
|
throw new Error("No valid fields to update");
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql = `UPDATE ${tableName} SET ${setClauses.join(", ")} WHERE ${whereClause}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
sql,
|
||||||
|
params: [...params, ...whereParams],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapQueryResultToValues(
|
||||||
|
record: QueryExecResult | undefined,
|
||||||
|
): Array<Record<string, unknown>> {
|
||||||
|
if (!record) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return mapColumnsToValues(record.columns, record.values) as Array<
|
||||||
|
Record<string, unknown>
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps an array of column names to an array of value arrays, creating objects where each column name
|
||||||
|
* is mapped to its corresponding value.
|
||||||
|
* @param columns Array of column names to use as object keys
|
||||||
|
* @param values Array of value arrays, where each inner array corresponds to one row of data
|
||||||
|
* @returns Array of objects where each object maps column names to their corresponding values
|
||||||
|
*/
|
||||||
|
export function mapColumnsToValues(
|
||||||
|
columns: string[],
|
||||||
|
values: unknown[][],
|
||||||
|
): Array<Record<string, unknown>> {
|
||||||
|
return values.map((row) => {
|
||||||
|
const obj: Record<string, unknown> = {};
|
||||||
|
columns.forEach((column, index) => {
|
||||||
|
obj[column] = row[index];
|
||||||
|
});
|
||||||
|
return obj;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,3 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* This is the original IndexedDB version of the database.
|
||||||
|
* It will eventually be replaced fully by the SQL version in databaseUtil.ts.
|
||||||
|
* Turn this on or off with the USE_DEXIE_DB constant in constants/app.ts.
|
||||||
|
*/
|
||||||
|
|
||||||
import BaseDexie, { Table } from "dexie";
|
import BaseDexie, { Table } from "dexie";
|
||||||
import { encrypted, Encryption } from "@pvermeer/dexie-encrypted-addon";
|
import { encrypted, Encryption } from "@pvermeer/dexie-encrypted-addon";
|
||||||
import * as R from "ramda";
|
import * as R from "ramda";
|
||||||
@@ -26,8 +32,8 @@ type NonsensitiveTables = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Using 'unknown' instead of 'any' for stricter typing and to avoid TypeScript warnings
|
// Using 'unknown' instead of 'any' for stricter typing and to avoid TypeScript warnings
|
||||||
export type SecretDexie<T extends unknown = SecretTable> = BaseDexie & T;
|
type SecretDexie<T extends unknown = SecretTable> = BaseDexie & T;
|
||||||
export type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T;
|
type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T;
|
||||||
export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> =
|
export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> =
|
||||||
BaseDexie & T;
|
BaseDexie & T;
|
||||||
|
|
||||||
@@ -90,21 +96,18 @@ db.on("populate", async () => {
|
|||||||
try {
|
try {
|
||||||
await db.settings.add(DEFAULT_SETTINGS);
|
await db.settings.add(DEFAULT_SETTINGS);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
logger.error("Error populating the database with default settings:", error);
|
||||||
"Error populating the database with default settings:",
|
|
||||||
error,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Helper function to safely open the database with retries
|
// Helper function to safely open the database with retries
|
||||||
async function safeOpenDatabase(retries = 1, delay = 500): Promise<void> {
|
async function safeOpenDatabase(retries = 1, delay = 500): Promise<void> {
|
||||||
// console.log("Starting safeOpenDatabase with retries:", retries);
|
// logger.log("Starting safeOpenDatabase with retries:", retries);
|
||||||
for (let i = 0; i < retries; i++) {
|
for (let i = 0; i < retries; i++) {
|
||||||
try {
|
try {
|
||||||
// console.log(`Attempt ${i + 1}: Checking if database is open...`);
|
// logger.log(`Attempt ${i + 1}: Checking if database is open...`);
|
||||||
if (!db.isOpen()) {
|
if (!db.isOpen()) {
|
||||||
// console.log(`Attempt ${i + 1}: Database is closed, attempting to open...`);
|
// logger.log(`Attempt ${i + 1}: Database is closed, attempting to open...`);
|
||||||
|
|
||||||
// Create a promise that rejects after 5 seconds
|
// Create a promise that rejects after 5 seconds
|
||||||
const timeoutPromise = new Promise((_, reject) => {
|
const timeoutPromise = new Promise((_, reject) => {
|
||||||
@@ -113,19 +116,19 @@ async function safeOpenDatabase(retries = 1, delay = 500): Promise<void> {
|
|||||||
|
|
||||||
// Race between the open operation and the timeout
|
// Race between the open operation and the timeout
|
||||||
const openPromise = db.open();
|
const openPromise = db.open();
|
||||||
// console.log(`Attempt ${i + 1}: Waiting for db.open() promise...`);
|
// logger.log(`Attempt ${i + 1}: Waiting for db.open() promise...`);
|
||||||
await Promise.race([openPromise, timeoutPromise]);
|
await Promise.race([openPromise, timeoutPromise]);
|
||||||
|
|
||||||
// If we get here, the open succeeded
|
// If we get here, the open succeeded
|
||||||
// console.log(`Attempt ${i + 1}: Database opened successfully`);
|
// logger.log(`Attempt ${i + 1}: Database opened successfully`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// console.log(`Attempt ${i + 1}: Database was already open`);
|
// logger.log(`Attempt ${i + 1}: Database was already open`);
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Attempt ${i + 1}: Database open failed:`, error);
|
logger.error(`Attempt ${i + 1}: Database open failed:`, error);
|
||||||
if (i < retries - 1) {
|
if (i < retries - 1) {
|
||||||
console.log(`Attempt ${i + 1}: Waiting ${delay}ms before retry...`);
|
logger.log(`Attempt ${i + 1}: Waiting ${delay}ms before retry...`);
|
||||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
} else {
|
} else {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -142,16 +145,14 @@ export async function updateDefaultSettings(
|
|||||||
delete settingsChanges.id;
|
delete settingsChanges.id;
|
||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
// console.log("Database state before open:", db.isOpen() ? "open" : "closed");
|
// logger.log("Database state before open:", db.isOpen() ? "open" : "closed");
|
||||||
// console.log("Database name:", db.name);
|
// logger.log("Database name:", db.name);
|
||||||
// console.log("Database version:", db.verno);
|
// logger.log("Database version:", db.verno);
|
||||||
await safeOpenDatabase();
|
await safeOpenDatabase();
|
||||||
} catch (openError: unknown) {
|
} catch (openError: unknown) {
|
||||||
console.error("Failed to open database:", openError);
|
logger.error("Failed to open database:", openError, String(openError));
|
||||||
const errorMessage =
|
|
||||||
openError instanceof Error ? openError.message : String(openError);
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Database connection failed: ${errorMessage}. Please try again or restart the app.`,
|
`The database connection failed. We recommend you try again or restart the app.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const result = await db.settings.update(
|
const result = await db.settings.update(
|
||||||
@@ -160,11 +161,13 @@ export async function updateDefaultSettings(
|
|||||||
);
|
);
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating default settings:", error);
|
logger.error("Error updating default settings:", 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 {
|
||||||
throw new Error(`Failed to update settings: ${error}`);
|
throw new Error(
|
||||||
|
`Failed to update settings. We recommend you try again or restart the app.`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,12 @@ export type Account = {
|
|||||||
publicKeyHex: string;
|
publicKeyHex: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// When finished with USE_DEXIE_DB, move these fields to Account and move identity and mnemonic here.
|
||||||
|
export type AccountEncrypted = Account & {
|
||||||
|
identityEncrBase64: string;
|
||||||
|
mnemonicEncrBase64: string;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schema for the accounts table in the database.
|
* Schema for the accounts table in the database.
|
||||||
* Fields starting with a $ character are encrypted.
|
* Fields starting with a $ character are encrypted.
|
||||||
|
|||||||
@@ -25,6 +25,25 @@ function createWindow(): void {
|
|||||||
logger.log("Checking preload path:", preloadPath);
|
logger.log("Checking preload path:", preloadPath);
|
||||||
logger.log("Preload exists:", fs.existsSync(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.
|
// Create the browser window.
|
||||||
const mainWindow = new BrowserWindow({
|
const mainWindow = new BrowserWindow({
|
||||||
width: 1200,
|
width: 1200,
|
||||||
@@ -88,7 +107,16 @@ function createWindow(): void {
|
|||||||
logger.log("process.cwd():", process.cwd());
|
logger.log("process.cwd():", process.cwd());
|
||||||
}
|
}
|
||||||
|
|
||||||
const indexPath = path.join(__dirname, "www", "index.html");
|
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) {
|
if (isDev) {
|
||||||
logger.log("Loading index from:", indexPath);
|
logger.log("Loading index from:", indexPath);
|
||||||
|
|||||||
@@ -2,24 +2,33 @@ const { contextBridge, ipcRenderer } = require("electron");
|
|||||||
|
|
||||||
const logger = {
|
const logger = {
|
||||||
log: (message, ...args) => {
|
log: (message, ...args) => {
|
||||||
|
// Always log in development, log with context in production
|
||||||
if (process.env.NODE_ENV !== "production") {
|
if (process.env.NODE_ENV !== "production") {
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
console.log(message, ...args);
|
console.log(`[Preload] ${message}`, ...args);
|
||||||
/* eslint-enable no-console */
|
/* eslint-enable no-console */
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
warn: (message, ...args) => {
|
warn: (message, ...args) => {
|
||||||
if (process.env.NODE_ENV !== "production") {
|
// Always log warnings
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
console.warn(message, ...args);
|
console.warn(`[Preload] ${message}`, ...args);
|
||||||
/* eslint-enable no-console */
|
/* eslint-enable no-console */
|
||||||
}
|
|
||||||
},
|
},
|
||||||
error: (message, ...args) => {
|
error: (message, ...args) => {
|
||||||
|
// Always log errors
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
console.error(message, ...args); // Errors should always be logged
|
console.error(`[Preload] ${message}`, ...args);
|
||||||
/* eslint-enable no-console */
|
/* 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
|
// Use a more direct path resolution approach
|
||||||
@@ -41,7 +50,10 @@ const getPath = (pathType) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.log("Preload script starting...");
|
logger.info("Preload script starting...");
|
||||||
|
|
||||||
|
// Force electron platform in the renderer process
|
||||||
|
window.process = { env: { VITE_PLATFORM: "electron" } };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
contextBridge.exposeInMainWorld("electronAPI", {
|
contextBridge.exposeInMainWorld("electronAPI", {
|
||||||
@@ -65,6 +77,7 @@ try {
|
|||||||
env: {
|
env: {
|
||||||
isElectron: true,
|
isElectron: true,
|
||||||
isDev: process.env.NODE_ENV === "development",
|
isDev: process.env.NODE_ENV === "development",
|
||||||
|
platform: "electron", // Explicitly set platform
|
||||||
},
|
},
|
||||||
// Path utilities
|
// Path utilities
|
||||||
getBasePath: () => {
|
getBasePath: () => {
|
||||||
@@ -72,7 +85,7 @@ try {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.log("Preload script completed successfully");
|
logger.info("Preload script completed successfully");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error in preload script:", error);
|
logger.error("Error in preload script:", error);
|
||||||
}
|
}
|
||||||
|
|||||||
59
src/interfaces/absurd-sql.d.ts
vendored
Normal file
59
src/interfaces/absurd-sql.d.ts
vendored
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import type { QueryExecResult, SqlValue } from "./database";
|
||||||
|
|
||||||
|
declare module "@jlongster/sql.js" {
|
||||||
|
interface SQL {
|
||||||
|
Database: new (path: string, options?: { filename: boolean }) => AbsurdSqlDatabase;
|
||||||
|
FS: {
|
||||||
|
mkdir: (path: string) => void;
|
||||||
|
mount: (fs: any, options: any, path: string) => void;
|
||||||
|
open: (path: string, flags: string) => any;
|
||||||
|
close: (stream: any) => void;
|
||||||
|
};
|
||||||
|
register_for_idb: (fs: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AbsurdSqlDatabase {
|
||||||
|
exec: (sql: string, params?: unknown[]) => Promise<QueryExecResult[]>;
|
||||||
|
run: (
|
||||||
|
sql: string,
|
||||||
|
params?: unknown[],
|
||||||
|
) => Promise<{ changes: number; lastId?: number }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initSqlJs: (options?: {
|
||||||
|
locateFile?: (file: string) => string;
|
||||||
|
}) => Promise<SQL>;
|
||||||
|
|
||||||
|
export default initSqlJs;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "absurd-sql" {
|
||||||
|
import type { SQL } from "@jlongster/sql.js";
|
||||||
|
|
||||||
|
export class SQLiteFS {
|
||||||
|
constructor(fs: any, backend: any);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "absurd-sql/dist/indexeddb-backend" {
|
||||||
|
export default class IndexedDBBackend {
|
||||||
|
constructor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "absurd-sql/dist/indexeddb-main-thread" {
|
||||||
|
export interface SQLiteOptions {
|
||||||
|
filename?: string;
|
||||||
|
autoLoad?: boolean;
|
||||||
|
debug?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SQLiteDatabase {
|
||||||
|
exec: (sql: string, params?: unknown[]) => Promise<QueryExecResult[]>;
|
||||||
|
close: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initSqlJs(options?: any): Promise<any>;
|
||||||
|
export function createDatabase(options?: SQLiteOptions): Promise<SQLiteDatabase>;
|
||||||
|
export function openDatabase(options?: SQLiteOptions): Promise<SQLiteDatabase>;
|
||||||
|
}
|
||||||
@@ -1,15 +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"?: string;
|
// 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;
|
||||||
@@ -17,16 +26,25 @@ 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 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface JoinActionClaim extends ClaimObject {
|
||||||
|
agent?: { identifier: string };
|
||||||
|
event?: { organizer?: { name: string }; name?: string; startTime?: 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"?: string;
|
"@context": "https://schema.org";
|
||||||
"@type": "Offer";
|
"@type": "Offer";
|
||||||
|
agent?: { identifier: string };
|
||||||
description?: string;
|
description?: string;
|
||||||
|
fulfills?: { "@type": string; identifier?: string; lastClaimId?: string }[];
|
||||||
|
identifier?: string;
|
||||||
|
image?: string;
|
||||||
includesObject?: { amountOfThisGood: number; unitCode: string };
|
includesObject?: { amountOfThisGood: number; unitCode: string };
|
||||||
itemOffered?: {
|
itemOffered?: {
|
||||||
description?: string;
|
description?: string;
|
||||||
@@ -37,14 +55,18 @@ export interface OfferVerifiableCredential extends GenericVerifiableCredential {
|
|||||||
name?: string;
|
name?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
offeredBy?: { identifier: string };
|
offeredBy?: {
|
||||||
|
type?: "Person";
|
||||||
|
identifier: string;
|
||||||
|
};
|
||||||
|
provider?: ClaimObject;
|
||||||
recipient?: { identifier: string };
|
recipient?: { identifier: string };
|
||||||
validThrough?: string;
|
validThrough?: 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;
|
||||||
@@ -58,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 } };
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,3 +34,77 @@ export interface ErrorResult extends ResultWithType {
|
|||||||
type: "error";
|
type: "error";
|
||||||
error: InternalError;
|
error: InternalError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface KeyMeta {
|
||||||
|
did: string;
|
||||||
|
publicKeyHex: string;
|
||||||
|
derivationPath?: string;
|
||||||
|
passkeyCredIdHex?: string; // The Webauthn credential ID in hex, if this is from a passkey
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
"@type": "QuantitativeValue";
|
||||||
|
"@context"?: string;
|
||||||
|
amountOfThisGood: number;
|
||||||
|
unitCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AxiosErrorResponse {
|
||||||
|
message?: string;
|
||||||
|
response?: {
|
||||||
|
data?: {
|
||||||
|
error?: {
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
status?: number;
|
||||||
|
config?: unknown;
|
||||||
|
};
|
||||||
|
config?: unknown;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserInfo {
|
||||||
|
did: string;
|
||||||
|
name: string;
|
||||||
|
publicEncKey: string;
|
||||||
|
registered: boolean;
|
||||||
|
profileImageUrl?: string;
|
||||||
|
nextPublicEncKeyHash?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateAndSubmitClaimResult {
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
handleId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Agent {
|
||||||
|
identifier?: string;
|
||||||
|
did?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClaimObject {
|
||||||
|
"@type": string;
|
||||||
|
"@context"?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VerifiableCredentialClaim {
|
||||||
|
"@context"?: string;
|
||||||
|
"@type": string;
|
||||||
|
type: string[];
|
||||||
|
credentialSubject: ClaimObject;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|||||||
15
src/interfaces/database.ts
Normal file
15
src/interfaces/database.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export type SqlValue = string | number | null | Uint8Array;
|
||||||
|
|
||||||
|
export interface QueryExecResult {
|
||||||
|
columns: Array<string>;
|
||||||
|
values: Array<Array<SqlValue>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DatabaseService {
|
||||||
|
initialize(): Promise<void>;
|
||||||
|
query(sql: string, params?: unknown[]): Promise<QueryExecResult[]>;
|
||||||
|
run(
|
||||||
|
sql: string,
|
||||||
|
params?: unknown[],
|
||||||
|
): Promise<{ changes: number; lastId?: number }>;
|
||||||
|
}
|
||||||
@@ -1,13 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Interfaces for the give records with limited contact information, good to show on a feed.
|
||||||
|
**/
|
||||||
|
|
||||||
import { GiveSummaryRecord } from "./records";
|
import { GiveSummaryRecord } from "./records";
|
||||||
|
|
||||||
// Common interface for contact information
|
// Common interface for views with summary contact information
|
||||||
export interface ContactInfo {
|
export interface ContactInfo {
|
||||||
known: boolean;
|
known: boolean;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
profileImageUrl?: string;
|
profileImageUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define the contact information fields
|
// Define a subset of contact information fields
|
||||||
interface GiveContactInfo {
|
interface GiveContactInfo {
|
||||||
giver: ContactInfo;
|
giver: ContactInfo;
|
||||||
issuer: ContactInfo;
|
issuer: ContactInfo;
|
||||||
@@ -17,5 +21,5 @@ interface GiveContactInfo {
|
|||||||
image?: string;
|
image?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combine GiveSummaryRecord with contact information using intersection type
|
// Combine GiveSummaryRecord with contact information
|
||||||
export type GiveRecordWithContactInfo = GiveSummaryRecord & GiveContactInfo;
|
export type GiveRecordWithContactInfo = GiveSummaryRecord & GiveContactInfo;
|
||||||
|
|||||||
@@ -1,7 +1,37 @@
|
|||||||
export * from "./claims";
|
export type {
|
||||||
export * from "./claims-result";
|
// From common.ts
|
||||||
export * from "./common";
|
GenericCredWrapper,
|
||||||
|
GenericVerifiableCredential,
|
||||||
|
KeyMeta,
|
||||||
|
// Exclude types that are also exported from other files
|
||||||
|
// GiveVerifiableCredential,
|
||||||
|
// OfferVerifiableCredential,
|
||||||
|
// RegisterVerifiableCredential,
|
||||||
|
// PlanSummaryRecord,
|
||||||
|
// UserInfo,
|
||||||
|
} from "./common";
|
||||||
|
|
||||||
|
export type {
|
||||||
|
// From claims.ts
|
||||||
|
GiveActionClaim,
|
||||||
|
OfferClaim,
|
||||||
|
RegisterActionClaim,
|
||||||
|
} from "./claims";
|
||||||
|
|
||||||
|
export type {
|
||||||
|
// From claims-result.ts
|
||||||
|
CreateAndSubmitClaimResult,
|
||||||
|
} from "./claims-result";
|
||||||
|
|
||||||
|
export type {
|
||||||
|
// From records.ts
|
||||||
|
PlanSummaryRecord,
|
||||||
|
} from "./records";
|
||||||
|
|
||||||
|
export type {
|
||||||
|
// From user.ts
|
||||||
|
UserInfo,
|
||||||
|
} from "./user";
|
||||||
|
|
||||||
export * from "./limits";
|
export * from "./limits";
|
||||||
export * from "./records";
|
|
||||||
export * from "./user";
|
|
||||||
export * from "./deepLinks";
|
export * from "./deepLinks";
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export const newIdentifier = (
|
|||||||
publicHex: string,
|
publicHex: string,
|
||||||
privateHex: string,
|
privateHex: string,
|
||||||
derivationPath: string,
|
derivationPath: string,
|
||||||
): Omit<IIdentifier, keyof "provider"> => {
|
): IIdentifier => {
|
||||||
return {
|
return {
|
||||||
did: DEFAULT_DID_PROVIDER_NAME + ":" + address,
|
did: DEFAULT_DID_PROVIDER_NAME + ":" + address,
|
||||||
keys: [
|
keys: [
|
||||||
@@ -159,7 +159,7 @@ export const nextDerivationPath = (origDerivPath: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Base64 encoding/decoding utilities for browser
|
// Base64 encoding/decoding utilities for browser
|
||||||
function base64ToArrayBuffer(base64: string): Uint8Array {
|
export function base64ToArrayBuffer(base64: string): Uint8Array {
|
||||||
const binaryString = atob(base64);
|
const binaryString = atob(base64);
|
||||||
const bytes = new Uint8Array(binaryString.length);
|
const bytes = new Uint8Array(binaryString.length);
|
||||||
for (let i = 0; i < binaryString.length; i++) {
|
for (let i = 0; i < binaryString.length; i++) {
|
||||||
@@ -168,7 +168,7 @@ function base64ToArrayBuffer(base64: string): Uint8Array {
|
|||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
function arrayBufferToBase64(buffer: ArrayBuffer): string {
|
export function arrayBufferToBase64(buffer: ArrayBuffer): string {
|
||||||
const binary = String.fromCharCode(...new Uint8Array(buffer));
|
const binary = String.fromCharCode(...new Uint8Array(buffer));
|
||||||
return btoa(binary);
|
return btoa(binary);
|
||||||
}
|
}
|
||||||
@@ -178,7 +178,7 @@ const IV_LENGTH = 12;
|
|||||||
const KEY_LENGTH = 256;
|
const KEY_LENGTH = 256;
|
||||||
const ITERATIONS = 100000;
|
const ITERATIONS = 100000;
|
||||||
|
|
||||||
// Encryption helper function
|
// Message encryption helper function, used for onboarding meeting messages
|
||||||
export async function encryptMessage(message: string, password: string) {
|
export async function encryptMessage(message: string, password: string) {
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
|
const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
|
||||||
@@ -226,7 +226,7 @@ export async function encryptMessage(message: string, password: string) {
|
|||||||
return btoa(JSON.stringify(result));
|
return btoa(JSON.stringify(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decryption helper function
|
// Message decryption helper function, used for onboarding meeting messages
|
||||||
export async function decryptMessage(encryptedJson: string, password: string) {
|
export async function decryptMessage(encryptedJson: string, password: string) {
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
const { salt, iv, encrypted } = JSON.parse(atob(encryptedJson));
|
const { salt, iv, encrypted } = JSON.parse(atob(encryptedJson));
|
||||||
@@ -273,7 +273,7 @@ export async function decryptMessage(encryptedJson: string, password: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Test function to verify encryption/decryption
|
// Test function to verify encryption/decryption
|
||||||
export async function testEncryptionDecryption() {
|
export async function testMessageEncryptionDecryption() {
|
||||||
try {
|
try {
|
||||||
const testMessage = "Hello, this is a test message! 🚀";
|
const testMessage = "Hello, this is a test message! 🚀";
|
||||||
const testPassword = "myTestPassword123";
|
const testPassword = "myTestPassword123";
|
||||||
@@ -299,9 +299,111 @@ export async function testEncryptionDecryption() {
|
|||||||
logger.log("\nTesting with wrong password...");
|
logger.log("\nTesting with wrong password...");
|
||||||
try {
|
try {
|
||||||
await decryptMessage(encrypted, "wrongPassword");
|
await decryptMessage(encrypted, "wrongPassword");
|
||||||
logger.log("Should not reach here");
|
logger.log("Incorrectly decrypted with wrong password ❌");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Correctly failed with wrong password ✅");
|
logger.log("Correctly failed to decrypt with wrong password ✅");
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Test failed with error:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple encryption using Node's crypto, used for the initial encryption of the identity and mnemonic
|
||||||
|
export async function simpleEncrypt(
|
||||||
|
text: string,
|
||||||
|
secret: ArrayBuffer,
|
||||||
|
): Promise<ArrayBuffer> {
|
||||||
|
const iv = crypto.getRandomValues(new Uint8Array(16));
|
||||||
|
|
||||||
|
// Derive a 256-bit key from the secret using SHA-256
|
||||||
|
const keyData = await crypto.subtle.digest("SHA-256", secret);
|
||||||
|
const key = await crypto.subtle.importKey(
|
||||||
|
"raw",
|
||||||
|
keyData,
|
||||||
|
{ name: "AES-GCM" },
|
||||||
|
false,
|
||||||
|
["encrypt"],
|
||||||
|
);
|
||||||
|
|
||||||
|
const encrypted = await crypto.subtle.encrypt(
|
||||||
|
{ name: "AES-GCM", iv },
|
||||||
|
key,
|
||||||
|
new TextEncoder().encode(text),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Combine IV and encrypted data
|
||||||
|
const result = new Uint8Array(iv.length + encrypted.byteLength);
|
||||||
|
result.set(iv);
|
||||||
|
result.set(new Uint8Array(encrypted), iv.length);
|
||||||
|
|
||||||
|
return result.buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple decryption using Node's crypto, used for the default decryption of identity and mnemonic
|
||||||
|
export async function simpleDecrypt(
|
||||||
|
encryptedText: ArrayBuffer,
|
||||||
|
secret: ArrayBuffer,
|
||||||
|
): Promise<string> {
|
||||||
|
const data = new Uint8Array(encryptedText);
|
||||||
|
|
||||||
|
// Extract IV and encrypted data
|
||||||
|
const iv = data.slice(0, 16);
|
||||||
|
const encrypted = data.slice(16);
|
||||||
|
|
||||||
|
// Derive the same 256-bit key from the secret using SHA-256
|
||||||
|
const keyData = await crypto.subtle.digest("SHA-256", secret);
|
||||||
|
const key = await crypto.subtle.importKey(
|
||||||
|
"raw",
|
||||||
|
keyData,
|
||||||
|
{ name: "AES-GCM" },
|
||||||
|
false,
|
||||||
|
["decrypt"],
|
||||||
|
);
|
||||||
|
|
||||||
|
const decrypted = await crypto.subtle.decrypt(
|
||||||
|
{ name: "AES-GCM", iv },
|
||||||
|
key,
|
||||||
|
encrypted,
|
||||||
|
);
|
||||||
|
|
||||||
|
return new TextDecoder().decode(decrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test function for simple encryption/decryption
|
||||||
|
export async function testSimpleEncryptionDecryption() {
|
||||||
|
try {
|
||||||
|
const testMessage = "Hello, this is a test message! 🚀";
|
||||||
|
const testSecret = crypto.getRandomValues(new Uint8Array(32));
|
||||||
|
|
||||||
|
logger.log("Original message:", testMessage);
|
||||||
|
|
||||||
|
// Test encryption
|
||||||
|
logger.log("Encrypting...");
|
||||||
|
const encrypted = await simpleEncrypt(testMessage, testSecret);
|
||||||
|
const encryptedBase64 = arrayBufferToBase64(encrypted);
|
||||||
|
logger.log("Encrypted result:", encryptedBase64);
|
||||||
|
|
||||||
|
// Test decryption
|
||||||
|
logger.log("Decrypting...");
|
||||||
|
const encryptedArrayBuffer = base64ToArrayBuffer(encryptedBase64);
|
||||||
|
const decrypted = await simpleDecrypt(encryptedArrayBuffer, testSecret);
|
||||||
|
logger.log("Decrypted result:", decrypted);
|
||||||
|
|
||||||
|
// Verify
|
||||||
|
const success = testMessage === decrypted;
|
||||||
|
logger.log("Test " + (success ? "PASSED ✅" : "FAILED ❌"));
|
||||||
|
logger.log("Messages match:", success);
|
||||||
|
|
||||||
|
// Test with wrong secret
|
||||||
|
logger.log("\nTesting with wrong secret...");
|
||||||
|
try {
|
||||||
|
await simpleDecrypt(encryptedArrayBuffer, new Uint8Array(32));
|
||||||
|
logger.log("Incorrectly decrypted with wrong secret ❌");
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("Correctly failed to decrypt with wrong secret ✅");
|
||||||
}
|
}
|
||||||
|
|
||||||
return success;
|
return success;
|
||||||
|
|||||||
@@ -17,29 +17,12 @@ 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, 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";
|
||||||
export const UNSUPPORTED_DID_METHOD_CODE = "UNSUPPORTED_DID_METHOD";
|
export const UNSUPPORTED_DID_METHOD_CODE = "UNSUPPORTED_DID_METHOD";
|
||||||
|
|
||||||
/**
|
|
||||||
* Meta info about a key
|
|
||||||
*/
|
|
||||||
export interface KeyMeta {
|
|
||||||
/**
|
|
||||||
* Decentralized ID for the key
|
|
||||||
*/
|
|
||||||
did: string;
|
|
||||||
/**
|
|
||||||
* Stringified IIDentifier object from Veramo
|
|
||||||
*/
|
|
||||||
identity?: string;
|
|
||||||
/**
|
|
||||||
* The Webauthn credential ID in hex, if this is from a passkey
|
|
||||||
*/
|
|
||||||
passkeyCredIdHex?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ethLocalResolver = new Resolver({ ethr: didEthLocalResolver });
|
const ethLocalResolver = new Resolver({ ethr: didEthLocalResolver });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,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,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Buffer } from "buffer/";
|
import { Buffer } from "buffer/";
|
||||||
import { JWTPayload } from "did-jwt";
|
import { JWTPayload } from "did-jwt";
|
||||||
import { DIDResolutionResult } from "did-resolver";
|
import { p256 } from "@noble/curves/p256";
|
||||||
import { sha256 } from "ethereum-cryptography/sha256.js";
|
|
||||||
import {
|
import {
|
||||||
startAuthentication,
|
startAuthentication,
|
||||||
startRegistration,
|
startRegistration,
|
||||||
@@ -11,12 +10,13 @@ import {
|
|||||||
generateRegistrationOptions,
|
generateRegistrationOptions,
|
||||||
verifyAuthenticationResponse,
|
verifyAuthenticationResponse,
|
||||||
verifyRegistrationResponse,
|
verifyRegistrationResponse,
|
||||||
|
VerifyAuthenticationResponseOpts,
|
||||||
} from "@simplewebauthn/server";
|
} from "@simplewebauthn/server";
|
||||||
import { VerifyAuthenticationResponseOpts } from "@simplewebauthn/server/esm/authentication/verifyAuthenticationResponse";
|
|
||||||
import {
|
import {
|
||||||
Base64URLString,
|
Base64URLString,
|
||||||
PublicKeyCredentialCreationOptionsJSON,
|
PublicKeyCredentialCreationOptionsJSON,
|
||||||
PublicKeyCredentialRequestOptionsJSON,
|
PublicKeyCredentialRequestOptionsJSON,
|
||||||
|
AuthenticatorAssertionResponse,
|
||||||
} from "@simplewebauthn/types";
|
} from "@simplewebauthn/types";
|
||||||
|
|
||||||
import { AppString } from "../../../constants/app";
|
import { AppString } from "../../../constants/app";
|
||||||
@@ -194,16 +194,19 @@ export class PeerSetup {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const credential = await navigator.credentials.get(options);
|
const credential = (await navigator.credentials.get(
|
||||||
|
options,
|
||||||
|
)) as PublicKeyCredential;
|
||||||
// console.log("nav credential get", credential);
|
// console.log("nav credential get", credential);
|
||||||
|
|
||||||
this.authenticatorData = credential?.response.authenticatorData;
|
const response = credential?.response as AuthenticatorAssertionResponse;
|
||||||
|
this.authenticatorData = response?.authenticatorData;
|
||||||
const authenticatorDataBase64Url = arrayBufferToBase64URLString(
|
const authenticatorDataBase64Url = arrayBufferToBase64URLString(
|
||||||
this.authenticatorData as ArrayBuffer,
|
this.authenticatorData as ArrayBuffer,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.clientDataJsonBase64Url = arrayBufferToBase64URLString(
|
this.clientDataJsonBase64Url = arrayBufferToBase64URLString(
|
||||||
credential?.response.clientDataJSON,
|
response?.clientDataJSON,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Our custom type of JWANT means the signature is based on a concatenation of the two Webauthn properties
|
// Our custom type of JWANT means the signature is based on a concatenation of the two Webauthn properties
|
||||||
@@ -228,9 +231,7 @@ export class PeerSetup {
|
|||||||
.replace(/\//g, "_")
|
.replace(/\//g, "_")
|
||||||
.replace(/=+$/, "");
|
.replace(/=+$/, "");
|
||||||
|
|
||||||
const origSignature = Buffer.from(credential?.response.signature).toString(
|
const origSignature = Buffer.from(response?.signature).toString("base64");
|
||||||
"base64",
|
|
||||||
);
|
|
||||||
this.signature = origSignature
|
this.signature = origSignature
|
||||||
.replace(/\+/g, "-")
|
.replace(/\+/g, "-")
|
||||||
.replace(/\//g, "_")
|
.replace(/\//g, "_")
|
||||||
@@ -315,24 +316,18 @@ export async function createDidPeerJwt(
|
|||||||
// ... and this import:
|
// ... and this import:
|
||||||
// import { p256 } from "@noble/curves/p256";
|
// import { p256 } from "@noble/curves/p256";
|
||||||
export async function verifyJwtP256(
|
export async function verifyJwtP256(
|
||||||
credIdHex: string,
|
|
||||||
issuerDid: string,
|
issuerDid: string,
|
||||||
authenticatorData: ArrayBuffer,
|
authenticatorData: ArrayBuffer,
|
||||||
challenge: Uint8Array,
|
challenge: Uint8Array,
|
||||||
clientDataJsonBase64Url: Base64URLString,
|
|
||||||
signature: Base64URLString,
|
signature: Base64URLString,
|
||||||
) {
|
) {
|
||||||
const authDataFromBase = Buffer.from(authenticatorData);
|
const authDataFromBase = Buffer.from(authenticatorData);
|
||||||
const clientDataFromBase = Buffer.from(clientDataJsonBase64Url, "base64");
|
|
||||||
const sigBuffer = Buffer.from(signature, "base64");
|
const sigBuffer = Buffer.from(signature, "base64");
|
||||||
const finalSigBuffer = unwrapEC2Signature(sigBuffer);
|
const finalSigBuffer = unwrapEC2Signature(sigBuffer);
|
||||||
const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid);
|
const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid);
|
||||||
|
|
||||||
// Hash the client data
|
// Use challenge in preimage construction
|
||||||
const hash = sha256(clientDataFromBase);
|
const preimage = Buffer.concat([authDataFromBase, Buffer.from(challenge)]);
|
||||||
|
|
||||||
// Construct the preimage
|
|
||||||
const preimage = Buffer.concat([authDataFromBase, hash]);
|
|
||||||
|
|
||||||
const isValid = p256.verify(
|
const isValid = p256.verify(
|
||||||
finalSigBuffer,
|
finalSigBuffer,
|
||||||
@@ -383,122 +378,37 @@ export async function verifyJwtSimplewebauthn(
|
|||||||
|
|
||||||
// similar code is in endorser-ch util-crypto.ts verifyPeerSignature
|
// similar code is in endorser-ch util-crypto.ts verifyPeerSignature
|
||||||
export async function verifyJwtWebCrypto(
|
export async function verifyJwtWebCrypto(
|
||||||
credId: Base64URLString,
|
|
||||||
issuerDid: string,
|
issuerDid: string,
|
||||||
authenticatorData: ArrayBuffer,
|
authenticatorData: ArrayBuffer,
|
||||||
challenge: Uint8Array,
|
challenge: Uint8Array,
|
||||||
clientDataJsonBase64Url: Base64URLString,
|
|
||||||
signature: Base64URLString,
|
signature: Base64URLString,
|
||||||
) {
|
) {
|
||||||
const authDataFromBase = Buffer.from(authenticatorData);
|
const authDataFromBase = Buffer.from(authenticatorData);
|
||||||
const clientDataFromBase = Buffer.from(clientDataJsonBase64Url, "base64");
|
|
||||||
const sigBuffer = Buffer.from(signature, "base64");
|
const sigBuffer = Buffer.from(signature, "base64");
|
||||||
const finalSigBuffer = unwrapEC2Signature(sigBuffer);
|
const finalSigBuffer = unwrapEC2Signature(sigBuffer);
|
||||||
|
|
||||||
// Hash the client data
|
// Use challenge in preimage construction
|
||||||
const hash = sha256(clientDataFromBase);
|
const preimage = Buffer.concat([authDataFromBase, Buffer.from(challenge)]);
|
||||||
|
|
||||||
// Construct the preimage
|
|
||||||
const preimage = Buffer.concat([authDataFromBase, hash]);
|
|
||||||
return verifyPeerSignature(preimage, issuerDid, finalSigBuffer);
|
return verifyPeerSignature(preimage, issuerDid, finalSigBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// Remove unused functions:
|
||||||
async function peerDidToDidDocument(did: string): Promise<DIDResolutionResult> {
|
// - peerDidToDidDocument
|
||||||
if (!did.startsWith("did:peer:0z")) {
|
// - COSEtoPEM
|
||||||
throw new Error(
|
// - base64urlDecodeArrayBuffer
|
||||||
"This only verifies a peer DID, method 0, encoded base58btc.",
|
// - base64urlEncodeArrayBuffer
|
||||||
);
|
// - pemToCryptoKey
|
||||||
}
|
|
||||||
// this is basically hard-coded from https://www.w3.org/TR/did-core/#example-various-verification-method-types
|
|
||||||
// (another reference is the @aviarytech/did-peer resolver)
|
|
||||||
|
|
||||||
/**
|
// Keep only the used functions:
|
||||||
* Looks like JsonWebKey2020 isn't too difficult:
|
|
||||||
* - change context security/suites link to jws-2020/v1
|
|
||||||
* - change publicKeyMultibase to publicKeyJwk generated with cborToKeys
|
|
||||||
* - change type to JsonWebKey2020
|
|
||||||
*/
|
|
||||||
|
|
||||||
const id = did.split(":")[2];
|
|
||||||
const multibase = id.slice(1);
|
|
||||||
const encnumbasis = multibase.slice(1);
|
|
||||||
const didDocument = {
|
|
||||||
"@context": [
|
|
||||||
"https://www.w3.org/ns/did/v1",
|
|
||||||
"https://w3id.org/security/suites/secp256k1-2019/v1",
|
|
||||||
],
|
|
||||||
assertionMethod: [did + "#" + encnumbasis],
|
|
||||||
authentication: [did + "#" + encnumbasis],
|
|
||||||
capabilityDelegation: [did + "#" + encnumbasis],
|
|
||||||
capabilityInvocation: [did + "#" + encnumbasis],
|
|
||||||
id: did,
|
|
||||||
keyAgreement: undefined,
|
|
||||||
service: undefined,
|
|
||||||
verificationMethod: [
|
|
||||||
{
|
|
||||||
controller: did,
|
|
||||||
id: did + "#" + encnumbasis,
|
|
||||||
publicKeyMultibase: multibase,
|
|
||||||
type: "EcdsaSecp256k1VerificationKey2019",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
didDocument,
|
|
||||||
didDocumentMetadata: {},
|
|
||||||
didResolutionMetadata: { contentType: "application/did+ld+json" },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// convert COSE public key to PEM format
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
function COSEtoPEM(cose: Buffer) {
|
|
||||||
// const alg = cose.get(3); // Algorithm
|
|
||||||
const x = cose[-2]; // x-coordinate
|
|
||||||
const y = cose[-3]; // y-coordinate
|
|
||||||
|
|
||||||
// Ensure the coordinates are in the correct format
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-expect-error because it complains about the type of x and y
|
|
||||||
const pubKeyBuffer = Buffer.concat([Buffer.from([0x04]), x, y]);
|
|
||||||
|
|
||||||
// Convert to PEM format
|
|
||||||
const pem = `-----BEGIN PUBLIC KEY-----
|
|
||||||
${pubKeyBuffer.toString("base64")}
|
|
||||||
-----END PUBLIC KEY-----`;
|
|
||||||
|
|
||||||
return pem;
|
|
||||||
}
|
|
||||||
|
|
||||||
// tried the base64url library but got an error using their Buffer
|
|
||||||
export function base64urlDecodeString(input: string) {
|
export function base64urlDecodeString(input: string) {
|
||||||
return atob(input.replace(/-/g, "+").replace(/_/g, "/"));
|
return atob(input.replace(/-/g, "+").replace(/_/g, "/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// tried the base64url library but got an error using their Buffer
|
|
||||||
export function base64urlEncodeString(input: string) {
|
export function base64urlEncodeString(input: string) {
|
||||||
return btoa(input).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
return btoa(input).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
function base64urlDecodeArrayBuffer(input: string) {
|
|
||||||
input = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
||||||
const pad = input.length % 4 === 0 ? "" : "====".slice(input.length % 4);
|
|
||||||
const str = atob(input + pad);
|
|
||||||
const bytes = new Uint8Array(str.length);
|
|
||||||
for (let i = 0; i < str.length; i++) {
|
|
||||||
bytes[i] = str.charCodeAt(i);
|
|
||||||
}
|
|
||||||
return bytes.buffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
function base64urlEncodeArrayBuffer(buffer: ArrayBuffer) {
|
|
||||||
const str = String.fromCharCode(...new Uint8Array(buffer));
|
|
||||||
return base64urlEncodeString(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
// from @simplewebauthn/browser
|
// from @simplewebauthn/browser
|
||||||
function arrayBufferToBase64URLString(buffer: ArrayBuffer) {
|
function arrayBufferToBase64URLString(buffer: ArrayBuffer) {
|
||||||
const bytes = new Uint8Array(buffer);
|
const bytes = new Uint8Array(buffer);
|
||||||
@@ -523,28 +433,3 @@ function base64URLStringToArrayBuffer(base64URLString: string) {
|
|||||||
}
|
}
|
||||||
return buffer;
|
return buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
async function pemToCryptoKey(pem: string) {
|
|
||||||
const binaryDerString = atob(
|
|
||||||
pem
|
|
||||||
.split("\n")
|
|
||||||
.filter((x) => !x.includes("-----"))
|
|
||||||
.join(""),
|
|
||||||
);
|
|
||||||
const binaryDer = new Uint8Array(binaryDerString.length);
|
|
||||||
for (let i = 0; i < binaryDerString.length; i++) {
|
|
||||||
binaryDer[i] = binaryDerString.charCodeAt(i);
|
|
||||||
}
|
|
||||||
// console.log("binaryDer", binaryDer.buffer);
|
|
||||||
return await window.crypto.subtle.importKey(
|
|
||||||
"spki",
|
|
||||||
binaryDer.buffer,
|
|
||||||
{
|
|
||||||
name: "RSASSA-PKCS1-v1_5",
|
|
||||||
hash: "SHA-256",
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
["verify"],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -26,29 +26,42 @@ import {
|
|||||||
DEFAULT_IMAGE_API_SERVER,
|
DEFAULT_IMAGE_API_SERVER,
|
||||||
NotificationIface,
|
NotificationIface,
|
||||||
APP_SERVER,
|
APP_SERVER,
|
||||||
|
USE_DEXIE_DB,
|
||||||
} from "../constants/app";
|
} from "../constants/app";
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from "../db/tables/contacts";
|
||||||
import { accessToken, deriveAddress, nextDerivationPath } from "../libs/crypto";
|
import { accessToken, deriveAddress, nextDerivationPath } from "../libs/crypto";
|
||||||
import { logConsoleAndDb, NonsensitiveDexie } from "../db/index";
|
import { NonsensitiveDexie } from "../db/index";
|
||||||
|
import { logConsoleAndDb } from "../db/databaseUtil";
|
||||||
import {
|
import {
|
||||||
retrieveAccountMetadata,
|
retrieveAccountMetadata,
|
||||||
retrieveFullyDecryptedAccount,
|
retrieveFullyDecryptedAccount,
|
||||||
getPasskeyExpirationSeconds,
|
getPasskeyExpirationSeconds,
|
||||||
} from "../libs/util";
|
} from "../libs/util";
|
||||||
import { createEndorserJwtForKey, KeyMeta } from "../libs/crypto/vc";
|
import { createEndorserJwtForKey } from "../libs/crypto/vc";
|
||||||
|
import {
|
||||||
|
GiveActionClaim,
|
||||||
|
JoinActionClaim,
|
||||||
|
OfferClaim,
|
||||||
|
PlanActionClaim,
|
||||||
|
RegisterActionClaim,
|
||||||
|
TenureClaim,
|
||||||
|
} from "../interfaces/claims";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
GiveVerifiableCredential,
|
|
||||||
OfferVerifiableCredential,
|
|
||||||
RegisterVerifiableCredential,
|
|
||||||
GenericVerifiableCredential,
|
|
||||||
GenericCredWrapper,
|
GenericCredWrapper,
|
||||||
PlanSummaryRecord,
|
GenericVerifiableCredential,
|
||||||
|
AxiosErrorResponse,
|
||||||
UserInfo,
|
UserInfo,
|
||||||
CreateAndSubmitClaimResult,
|
CreateAndSubmitClaimResult,
|
||||||
} from "../interfaces";
|
ClaimObject,
|
||||||
|
VerifiableCredentialClaim,
|
||||||
|
QuantitativeValue,
|
||||||
|
KeyMetaWithPrivate,
|
||||||
|
KeyMetaMaybeWithPrivate,
|
||||||
|
} from "../interfaces/common";
|
||||||
|
import { PlanSummaryRecord } from "../interfaces/records";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Standard context for schema.org data
|
* Standard context for schema.org data
|
||||||
@@ -100,7 +113,10 @@ export const ENDORSER_CH_HANDLE_PREFIX = "https://endorser.ch/entity/";
|
|||||||
|
|
||||||
export const BLANK_GENERIC_SERVER_RECORD: GenericCredWrapper<GenericVerifiableCredential> =
|
export const BLANK_GENERIC_SERVER_RECORD: GenericCredWrapper<GenericVerifiableCredential> =
|
||||||
{
|
{
|
||||||
claim: { "@type": "" },
|
claim: {
|
||||||
|
"@context": SCHEMA_ORG_CONTEXT,
|
||||||
|
"@type": "",
|
||||||
|
},
|
||||||
handleId: "",
|
handleId: "",
|
||||||
id: "",
|
id: "",
|
||||||
issuedAt: "",
|
issuedAt: "",
|
||||||
@@ -125,7 +141,7 @@ export function isDid(did: string): boolean {
|
|||||||
* @param {string} did - The DID to check
|
* @param {string} did - The DID to check
|
||||||
* @returns {boolean} True if DID is hidden
|
* @returns {boolean} True if DID is hidden
|
||||||
*/
|
*/
|
||||||
export function isHiddenDid(did: string): boolean {
|
export function isHiddenDid(did: string | undefined): boolean {
|
||||||
return did === HIDDEN_DID;
|
return did === HIDDEN_DID;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,37 +196,21 @@ export function isEmptyOrHiddenDid(did?: string): boolean {
|
|||||||
* };
|
* };
|
||||||
* testRecursivelyOnStrings(isHiddenDid, obj); // Returns: true
|
* testRecursivelyOnStrings(isHiddenDid, obj); // Returns: true
|
||||||
*/
|
*/
|
||||||
function testRecursivelyOnStrings(
|
const testRecursivelyOnStrings = (
|
||||||
func: (arg0: unknown) => boolean,
|
|
||||||
input: unknown,
|
input: unknown,
|
||||||
): boolean {
|
test: (s: string) => boolean,
|
||||||
// Test direct string values
|
): boolean => {
|
||||||
if (Object.prototype.toString.call(input) === "[object String]") {
|
if (typeof input === "string") {
|
||||||
return func(input);
|
return test(input);
|
||||||
|
} else if (Array.isArray(input)) {
|
||||||
|
return input.some((item) => testRecursivelyOnStrings(item, test));
|
||||||
|
} else if (input && typeof input === "object") {
|
||||||
|
return Object.values(input as Record<string, unknown>).some((value) =>
|
||||||
|
testRecursivelyOnStrings(value, test),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// Recursively test objects and arrays
|
return false;
|
||||||
else if (input instanceof Object) {
|
};
|
||||||
if (!Array.isArray(input)) {
|
|
||||||
// Handle plain objects
|
|
||||||
for (const key in input) {
|
|
||||||
if (testRecursivelyOnStrings(func, input[key])) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Handle arrays
|
|
||||||
for (const value of input) {
|
|
||||||
if (testRecursivelyOnStrings(func, value)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
// Non-string, non-object values can't contain strings
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export function containsHiddenDid(obj: any) {
|
export function containsHiddenDid(obj: any) {
|
||||||
@@ -551,7 +551,11 @@ export async function setPlanInCache(
|
|||||||
* @returns {string|undefined} User-friendly message or undefined if none found
|
* @returns {string|undefined} User-friendly message or undefined if none found
|
||||||
*/
|
*/
|
||||||
export function serverMessageForUser(error: unknown): string | undefined {
|
export function serverMessageForUser(error: unknown): string | undefined {
|
||||||
return error?.response?.data?.error?.message;
|
if (error && typeof error === "object" && "response" in error) {
|
||||||
|
const err = error as AxiosErrorResponse;
|
||||||
|
return err.response?.data?.error?.message;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -573,18 +577,27 @@ export function errorStringForLog(error: unknown) {
|
|||||||
// --- property '_value' closes the circle
|
// --- property '_value' closes the circle
|
||||||
}
|
}
|
||||||
let fullError = "" + error + " - JSON: " + stringifiedError;
|
let fullError = "" + error + " - JSON: " + stringifiedError;
|
||||||
const errorResponseText = JSON.stringify(error.response);
|
|
||||||
// for some reason, error.response is not included in stringify result (eg. for 400 errors on invite redemptions)
|
if (error && typeof error === "object" && "response" in error) {
|
||||||
if (!R.empty(errorResponseText) && !fullError.includes(errorResponseText)) {
|
const err = error as AxiosErrorResponse;
|
||||||
// add error.response stuff
|
const errorResponseText = JSON.stringify(err.response);
|
||||||
if (R.equals(error?.config, error?.response?.config)) {
|
// for some reason, error.response is not included in stringify result (eg. for 400 errors on invite redemptions)
|
||||||
// but exclude "config" because it's already in there
|
if (!R.empty(errorResponseText) && !fullError.includes(errorResponseText)) {
|
||||||
const newErrorResponseText = JSON.stringify(
|
// add error.response stuff
|
||||||
R.omit(["config"] as never[], error.response),
|
if (
|
||||||
);
|
err.response?.config &&
|
||||||
fullError += " - .response w/o same config JSON: " + newErrorResponseText;
|
err.config &&
|
||||||
} else {
|
R.equals(err.config, err.response.config)
|
||||||
fullError += " - .response JSON: " + errorResponseText;
|
) {
|
||||||
|
// but exclude "config" because it's already in there
|
||||||
|
const newErrorResponseText = JSON.stringify(
|
||||||
|
R.omit(["config"] as never[], err.response),
|
||||||
|
);
|
||||||
|
fullError +=
|
||||||
|
" - .response w/o same config JSON: " + newErrorResponseText;
|
||||||
|
} else {
|
||||||
|
fullError += " - .response JSON: " + errorResponseText;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fullError;
|
return fullError;
|
||||||
@@ -642,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,
|
||||||
@@ -650,14 +663,12 @@ export function hydrateGive(
|
|||||||
unitCode?: string,
|
unitCode?: string,
|
||||||
fulfillsProjectHandleId?: string,
|
fulfillsProjectHandleId?: string,
|
||||||
fulfillsOfferHandleId?: string,
|
fulfillsOfferHandleId?: string,
|
||||||
isTrade: boolean = false, // remove, because this app is all for gifting
|
isTrade: boolean = false,
|
||||||
imageUrl?: string,
|
imageUrl?: string,
|
||||||
providerPlanHandleId?: string,
|
providerPlanHandleId?: string,
|
||||||
lastClaimId?: string,
|
lastClaimId?: string,
|
||||||
): GiveVerifiableCredential {
|
): GiveActionClaim {
|
||||||
// Remember: replace values or erase if it's null
|
const vcClaim: GiveActionClaim = vcClaimOrig
|
||||||
|
|
||||||
const vcClaim: GiveVerifiableCredential = vcClaimOrig
|
|
||||||
? R.clone(vcClaimOrig)
|
? R.clone(vcClaimOrig)
|
||||||
: {
|
: {
|
||||||
"@context": SCHEMA_ORG_CONTEXT,
|
"@context": SCHEMA_ORG_CONTEXT,
|
||||||
@@ -665,55 +676,71 @@ export function hydrateGive(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (lastClaimId) {
|
if (lastClaimId) {
|
||||||
// this is an edit
|
|
||||||
vcClaim.lastClaimId = lastClaimId;
|
vcClaim.lastClaimId = lastClaimId;
|
||||||
delete vcClaim.identifier;
|
delete vcClaim.identifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
vcClaim.agent = fromDid ? { identifier: fromDid } : undefined;
|
if (fromDid) {
|
||||||
vcClaim.recipient = toDid ? { identifier: toDid } : undefined;
|
vcClaim.agent = { identifier: fromDid };
|
||||||
|
}
|
||||||
|
if (toDid) {
|
||||||
|
vcClaim.recipient = { identifier: toDid };
|
||||||
|
}
|
||||||
vcClaim.description = description || undefined;
|
vcClaim.description = description || undefined;
|
||||||
vcClaim.object =
|
|
||||||
amount && !isNaN(amount)
|
|
||||||
? { amountOfThisGood: amount, unitCode: unitCode || "HUR" }
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
// ensure fulfills is an array
|
if (amount && !isNaN(amount)) {
|
||||||
|
const quantitativeValue: QuantitativeValue = {
|
||||||
|
amountOfThisGood: amount,
|
||||||
|
unitCode: unitCode || "HUR",
|
||||||
|
};
|
||||||
|
vcClaim.object = quantitativeValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize fulfills array if not present
|
||||||
if (!Array.isArray(vcClaim.fulfills)) {
|
if (!Array.isArray(vcClaim.fulfills)) {
|
||||||
vcClaim.fulfills = vcClaim.fulfills ? [vcClaim.fulfills] : [];
|
vcClaim.fulfills = vcClaim.fulfills ? [vcClaim.fulfills] : [];
|
||||||
}
|
}
|
||||||
// ... and replace or add each element, ending with Trade or Donate
|
|
||||||
// I realize the following doesn't change any elements that are not PlanAction or Offer or Trade/Action.
|
// Filter and add fulfills elements
|
||||||
vcClaim.fulfills = vcClaim.fulfills.filter(
|
vcClaim.fulfills = vcClaim.fulfills.filter(
|
||||||
(elem) => elem["@type"] !== "PlanAction",
|
(elem: { "@type": string }) => elem["@type"] !== "PlanAction",
|
||||||
);
|
);
|
||||||
|
|
||||||
if (fulfillsProjectHandleId) {
|
if (fulfillsProjectHandleId) {
|
||||||
vcClaim.fulfills.push({
|
vcClaim.fulfills.push({
|
||||||
"@type": "PlanAction",
|
"@type": "PlanAction",
|
||||||
identifier: fulfillsProjectHandleId,
|
identifier: fulfillsProjectHandleId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
vcClaim.fulfills = vcClaim.fulfills.filter(
|
vcClaim.fulfills = vcClaim.fulfills.filter(
|
||||||
(elem) => elem["@type"] !== "Offer",
|
(elem: { "@type": string }) => elem["@type"] !== "Offer",
|
||||||
);
|
);
|
||||||
|
|
||||||
if (fulfillsOfferHandleId) {
|
if (fulfillsOfferHandleId) {
|
||||||
vcClaim.fulfills.push({
|
vcClaim.fulfills.push({
|
||||||
"@type": "Offer",
|
"@type": "Offer",
|
||||||
identifier: fulfillsOfferHandleId,
|
identifier: fulfillsOfferHandleId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// do Trade/Donate last because current endorser.ch only looks at the first for plans & offers
|
|
||||||
vcClaim.fulfills = vcClaim.fulfills.filter(
|
vcClaim.fulfills = vcClaim.fulfills.filter(
|
||||||
(elem) =>
|
(elem: { "@type": string }) =>
|
||||||
elem["@type"] !== "DonateAction" && elem["@type"] !== "TradeAction",
|
elem["@type"] !== "DonateAction" && elem["@type"] !== "TradeAction",
|
||||||
);
|
);
|
||||||
vcClaim.fulfills.push({ "@type": isTrade ? "TradeAction" : "DonateAction" });
|
|
||||||
|
vcClaim.fulfills.push({
|
||||||
|
"@type": isTrade ? "TradeAction" : "DonateAction",
|
||||||
|
});
|
||||||
|
|
||||||
vcClaim.image = imageUrl || undefined;
|
vcClaim.image = imageUrl || undefined;
|
||||||
|
|
||||||
vcClaim.provider = providerPlanHandleId
|
if (providerPlanHandleId) {
|
||||||
? { "@type": "PlanAction", identifier: providerPlanHandleId }
|
vcClaim.provider = {
|
||||||
: undefined;
|
"@type": "PlanAction",
|
||||||
|
identifier: providerPlanHandleId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return vcClaim;
|
return vcClaim;
|
||||||
}
|
}
|
||||||
@@ -774,7 +801,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,
|
||||||
@@ -815,7 +842,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,
|
||||||
@@ -825,10 +852,8 @@ export function hydrateOffer(
|
|||||||
fulfillsProjectHandleId?: string,
|
fulfillsProjectHandleId?: string,
|
||||||
validThrough?: string,
|
validThrough?: string,
|
||||||
lastClaimId?: string,
|
lastClaimId?: string,
|
||||||
): OfferVerifiableCredential {
|
): OfferClaim {
|
||||||
// Remember: replace values or erase if it's null
|
const vcClaim: OfferClaim = vcClaimOrig
|
||||||
|
|
||||||
const vcClaim: OfferVerifiableCredential = vcClaimOrig
|
|
||||||
? R.clone(vcClaimOrig)
|
? R.clone(vcClaimOrig)
|
||||||
: {
|
: {
|
||||||
"@context": SCHEMA_ORG_CONTEXT,
|
"@context": SCHEMA_ORG_CONTEXT,
|
||||||
@@ -841,14 +866,20 @@ export function hydrateOffer(
|
|||||||
delete vcClaim.identifier;
|
delete vcClaim.identifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
vcClaim.offeredBy = fromDid ? { identifier: fromDid } : undefined;
|
if (fromDid) {
|
||||||
vcClaim.recipient = toDid ? { identifier: toDid } : undefined;
|
vcClaim.offeredBy = { identifier: fromDid };
|
||||||
|
}
|
||||||
|
if (toDid) {
|
||||||
|
vcClaim.recipient = { identifier: toDid };
|
||||||
|
}
|
||||||
vcClaim.description = conditionDescription || undefined;
|
vcClaim.description = conditionDescription || undefined;
|
||||||
|
|
||||||
vcClaim.includesObject =
|
if (amount && !isNaN(amount)) {
|
||||||
amount && !isNaN(amount)
|
vcClaim.includesObject = {
|
||||||
? { amountOfThisGood: amount, unitCode: unitCode || "HUR" }
|
amountOfThisGood: amount,
|
||||||
: undefined;
|
unitCode: unitCode || "HUR",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (itemDescription || fulfillsProjectHandleId) {
|
if (itemDescription || fulfillsProjectHandleId) {
|
||||||
vcClaim.itemOffered = vcClaim.itemOffered || {};
|
vcClaim.itemOffered = vcClaim.itemOffered || {};
|
||||||
@@ -860,6 +891,7 @@ export function hydrateOffer(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
vcClaim.validThrough = validThrough || undefined;
|
vcClaim.validThrough = validThrough || undefined;
|
||||||
|
|
||||||
return vcClaim;
|
return vcClaim;
|
||||||
@@ -899,7 +931,7 @@ export async function createAndSubmitOffer(
|
|||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
return createAndSubmitClaim(
|
return createAndSubmitClaim(
|
||||||
vcClaim as OfferVerifiableCredential,
|
vcClaim as OfferClaim,
|
||||||
issuerDid,
|
issuerDid,
|
||||||
apiServer,
|
apiServer,
|
||||||
axios,
|
axios,
|
||||||
@@ -909,7 +941,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,
|
||||||
@@ -932,7 +964,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,
|
||||||
@@ -988,26 +1020,25 @@ export async function createAndSubmitClaim(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return { type: "success", response };
|
return { success: true, handleId: response.data?.handleId };
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
} catch (error: unknown) {
|
||||||
} catch (error: any) {
|
|
||||||
logger.error("Error submitting claim:", error);
|
logger.error("Error submitting claim:", error);
|
||||||
const errorMessage: string =
|
const errorMessage: string =
|
||||||
serverMessageForUser(error) ||
|
serverMessageForUser(error) ||
|
||||||
error.message ||
|
(error && typeof error === "object" && "message" in error
|
||||||
|
? String(error.message)
|
||||||
|
: undefined) ||
|
||||||
"Got some error submitting the claim. Check your permissions, network, and error logs.";
|
"Got some error submitting the claim. Check your permissions, network, and error logs.";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: "error",
|
success: false,
|
||||||
error: {
|
error: errorMessage,
|
||||||
error: errorMessage,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateEndorserJwtUrlForAccount(
|
export async function generateEndorserJwtUrlForAccount(
|
||||||
account: KeyMeta,
|
account: KeyMetaMaybeWithPrivate,
|
||||||
isRegistered: boolean,
|
isRegistered: boolean,
|
||||||
givenName: string,
|
givenName: string,
|
||||||
profileImageUrl: string,
|
profileImageUrl: string,
|
||||||
@@ -1031,12 +1062,9 @@ 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 && account?.mnemonic && account?.derivationPath) {
|
if (isContact && account.derivationPath && account.mnemonic) {
|
||||||
const newDerivPath = nextDerivationPath(account.derivationPath as string);
|
const newDerivPath = nextDerivationPath(account.derivationPath);
|
||||||
const nextPublicHex = deriveAddress(
|
const nextPublicHex = deriveAddress(account.mnemonic, newDerivPath)[2];
|
||||||
account.mnemonic as string,
|
|
||||||
newDerivPath,
|
|
||||||
)[2];
|
|
||||||
const nextPublicEncKey = Buffer.from(nextPublicHex, "hex");
|
const nextPublicEncKey = Buffer.from(nextPublicHex, "hex");
|
||||||
const nextPublicEncKeyHash = sha256(nextPublicEncKey);
|
const nextPublicEncKeyHash = sha256(nextPublicEncKey);
|
||||||
const nextPublicEncKeyHashBase64 =
|
const nextPublicEncKeyHashBase64 =
|
||||||
@@ -1056,7 +1084,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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1104,21 +1136,21 @@ export const capitalizeAndInsertSpacesBeforeCaps = (text: string) => {
|
|||||||
|
|
||||||
similar code is also contained in endorser-mobile
|
similar code is also contained in endorser-mobile
|
||||||
**/
|
**/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const claimSummary = (
|
const claimSummary = (
|
||||||
claim: GenericCredWrapper<GenericVerifiableCredential>,
|
claim:
|
||||||
|
| GenericVerifiableCredential
|
||||||
|
| GenericCredWrapper<GenericVerifiableCredential>,
|
||||||
) => {
|
) => {
|
||||||
if (!claim) {
|
if (!claim) {
|
||||||
// to differentiate from "something" above
|
|
||||||
return "something";
|
return "something";
|
||||||
}
|
}
|
||||||
let specificClaim:
|
let specificClaim: GenericVerifiableCredential;
|
||||||
| GenericVerifiableCredential
|
if ("claim" in claim) {
|
||||||
| GenericCredWrapper<GenericVerifiableCredential> = claim;
|
// It's a GenericCredWrapper
|
||||||
if (claim.claim) {
|
specificClaim = claim.claim as GenericVerifiableCredential;
|
||||||
// probably a Verified Credential
|
} else {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// It's already a GenericVerifiableCredential
|
||||||
specificClaim = claim.claim;
|
specificClaim = claim;
|
||||||
}
|
}
|
||||||
if (Array.isArray(specificClaim)) {
|
if (Array.isArray(specificClaim)) {
|
||||||
if (specificClaim.length === 1) {
|
if (specificClaim.length === 1) {
|
||||||
@@ -1153,88 +1185,112 @@ export const claimSpecialDescription = (
|
|||||||
identifiers: Array<string>,
|
identifiers: Array<string>,
|
||||||
contacts: Array<Contact>,
|
contacts: Array<Contact>,
|
||||||
) => {
|
) => {
|
||||||
let claim = record.claim;
|
let claim:
|
||||||
if (claim.claim) {
|
| GenericVerifiableCredential
|
||||||
// it's probably a Verified Credential
|
| GenericCredWrapper<GenericVerifiableCredential> = record.claim;
|
||||||
claim = claim.claim;
|
if ("claim" in claim) {
|
||||||
|
// it's a nested GenericCredWrapper
|
||||||
|
claim = claim.claim as GenericVerifiableCredential;
|
||||||
}
|
}
|
||||||
|
|
||||||
const issuer = didInfo(record.issuer, activeDid, identifiers, contacts);
|
const issuer = didInfo(record.issuer, activeDid, identifiers, contacts);
|
||||||
const type = claim["@type"] || "UnknownType";
|
const type = claim["@type"] || "UnknownType";
|
||||||
|
|
||||||
if (type === "AgreeAction") {
|
if (type === "AgreeAction") {
|
||||||
return issuer + " agreed with " + claimSummary(claim.object);
|
return (
|
||||||
|
issuer +
|
||||||
|
" agreed with " +
|
||||||
|
claimSummary(claim.object as GenericVerifiableCredential)
|
||||||
|
);
|
||||||
} else if (isAccept(claim)) {
|
} else if (isAccept(claim)) {
|
||||||
return issuer + " accepted " + claimSummary(claim.object);
|
return (
|
||||||
|
issuer +
|
||||||
|
" accepted " +
|
||||||
|
claimSummary(claim.object as GenericVerifiableCredential)
|
||||||
|
);
|
||||||
} else if (type === "GiveAction") {
|
} else if (type === "GiveAction") {
|
||||||
// agent.did is for legacy data, before March 2023
|
const giveClaim = claim as GiveActionClaim;
|
||||||
const giver = claim.agent?.identifier || claim.agent?.did;
|
// @ts-expect-error because .did may be found in legacy data, before March 2023
|
||||||
|
const legacyGiverDid = giveClaim.agent?.did;
|
||||||
|
const giver = giveClaim.agent?.identifier || legacyGiverDid;
|
||||||
const giverInfo = didInfo(giver, activeDid, identifiers, contacts);
|
const giverInfo = didInfo(giver, activeDid, identifiers, contacts);
|
||||||
let gaveAmount = claim.object?.amountOfThisGood
|
let gaveAmount = giveClaim.object?.amountOfThisGood
|
||||||
? displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
|
? displayAmount(
|
||||||
|
giveClaim.object.unitCode as string,
|
||||||
|
giveClaim.object.amountOfThisGood as number,
|
||||||
|
)
|
||||||
: "";
|
: "";
|
||||||
if (claim.description) {
|
if (giveClaim.description) {
|
||||||
if (gaveAmount) {
|
if (gaveAmount) {
|
||||||
gaveAmount = gaveAmount + ", and also: ";
|
gaveAmount = gaveAmount + ", and also: ";
|
||||||
}
|
}
|
||||||
gaveAmount = gaveAmount + claim.description;
|
gaveAmount = gaveAmount + giveClaim.description;
|
||||||
}
|
}
|
||||||
if (!gaveAmount) {
|
if (!gaveAmount) {
|
||||||
gaveAmount = "something not described";
|
gaveAmount = "something not described";
|
||||||
}
|
}
|
||||||
// recipient.did is for legacy data, before March 2023
|
// @ts-expect-error because .did may be found in legacy data, before March 2023
|
||||||
const gaveRecipientId = claim.recipient?.identifier || claim.recipient?.did;
|
const legacyRecipDid = giveClaim.recipient?.did;
|
||||||
|
const gaveRecipientId = giveClaim.recipient?.identifier || legacyRecipDid;
|
||||||
const gaveRecipientInfo = gaveRecipientId
|
const gaveRecipientInfo = gaveRecipientId
|
||||||
? " to " + didInfo(gaveRecipientId, activeDid, identifiers, contacts)
|
? " to " + didInfo(gaveRecipientId, activeDid, identifiers, contacts)
|
||||||
: "";
|
: "";
|
||||||
return giverInfo + " gave" + gaveRecipientInfo + ": " + gaveAmount;
|
return giverInfo + " gave" + gaveRecipientInfo + ": " + gaveAmount;
|
||||||
} else if (type === "JoinAction") {
|
} else if (type === "JoinAction") {
|
||||||
// agent.did is for legacy data, before March 2023
|
const joinClaim = claim as JoinActionClaim;
|
||||||
const agent = claim.agent?.identifier || claim.agent?.did;
|
// @ts-expect-error because .did may be found in legacy data, before March 2023
|
||||||
|
const legacyDid = joinClaim.agent?.did;
|
||||||
|
const agent = joinClaim.agent?.identifier || legacyDid;
|
||||||
const contactInfo = didInfo(agent, activeDid, identifiers, contacts);
|
const contactInfo = didInfo(agent, activeDid, identifiers, contacts);
|
||||||
|
|
||||||
let eventOrganizer =
|
let eventOrganizer =
|
||||||
claim.event && claim.event.organizer && claim.event.organizer.name;
|
joinClaim.event &&
|
||||||
|
joinClaim.event.organizer &&
|
||||||
|
joinClaim.event.organizer.name;
|
||||||
eventOrganizer = eventOrganizer || "";
|
eventOrganizer = eventOrganizer || "";
|
||||||
let eventName = claim.event && claim.event.name;
|
let eventName = joinClaim.event && joinClaim.event.name;
|
||||||
eventName = eventName ? " " + eventName : "";
|
eventName = eventName ? " " + eventName : "";
|
||||||
let fullEvent = eventOrganizer + eventName;
|
let fullEvent = eventOrganizer + eventName;
|
||||||
fullEvent = fullEvent ? " attended the " + fullEvent : "";
|
fullEvent = fullEvent ? " attended the " + fullEvent : "";
|
||||||
|
|
||||||
let eventDate = claim.event && claim.event.startTime;
|
let eventDate = joinClaim.event && joinClaim.event.startTime;
|
||||||
eventDate = eventDate ? " at " + eventDate : "";
|
eventDate = eventDate ? " at " + eventDate : "";
|
||||||
return contactInfo + fullEvent + eventDate;
|
return contactInfo + fullEvent + eventDate;
|
||||||
} else if (isOffer(claim)) {
|
} else if (isOffer(claim)) {
|
||||||
const offerer = claim.offeredBy?.identifier;
|
const offerClaim = claim as OfferClaim;
|
||||||
|
const offerer = offerClaim.offeredBy?.identifier;
|
||||||
const contactInfo = didInfo(offerer, activeDid, identifiers, contacts);
|
const contactInfo = didInfo(offerer, activeDid, identifiers, contacts);
|
||||||
let offering = "";
|
let offering = "";
|
||||||
if (claim.includesObject) {
|
if (offerClaim.includesObject) {
|
||||||
offering +=
|
offering +=
|
||||||
" " +
|
" " +
|
||||||
displayAmount(
|
displayAmount(
|
||||||
claim.includesObject.unitCode,
|
offerClaim.includesObject.unitCode,
|
||||||
claim.includesObject.amountOfThisGood,
|
offerClaim.includesObject.amountOfThisGood,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (claim.itemOffered?.description) {
|
if (offerClaim.itemOffered?.description) {
|
||||||
offering += ", saying: " + claim.itemOffered?.description;
|
offering += ", saying: " + offerClaim.itemOffered?.description;
|
||||||
}
|
}
|
||||||
// recipient.did is for legacy data, before March 2023
|
// @ts-expect-error because .did may be found in legacy data, before March 2023
|
||||||
const offerRecipientId =
|
const legacyDid = offerClaim.recipient?.did;
|
||||||
claim.recipient?.identifier || claim.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 claimer = claim.agent?.identifier || record.issuer;
|
const planClaim = claim as PlanActionClaim;
|
||||||
|
const claimer = planClaim.agent?.identifier || record.issuer;
|
||||||
const claimerInfo = didInfo(claimer, activeDid, identifiers, contacts);
|
const claimerInfo = didInfo(claimer, activeDid, identifiers, contacts);
|
||||||
return claimerInfo + " announced a project: " + claim.name;
|
return claimerInfo + " announced a project: " + planClaim.name;
|
||||||
} else if (type === "Tenure") {
|
} else if (type === "Tenure") {
|
||||||
// party.did is for legacy data, before March 2023
|
const tenureClaim = claim as TenureClaim;
|
||||||
const claimer = claim.party?.identifier || claim.party?.did;
|
// @ts-expect-error because .did may be found in legacy data, before March 2023
|
||||||
|
const legacyDid = tenureClaim.party?.did;
|
||||||
|
const claimer = tenureClaim.party?.identifier || legacyDid;
|
||||||
const contactInfo = didInfo(claimer, activeDid, identifiers, contacts);
|
const contactInfo = didInfo(claimer, activeDid, identifiers, contacts);
|
||||||
const polygon = claim.spatialUnit?.geo?.polygon || "";
|
const polygon = tenureClaim.spatialUnit?.geo?.polygon || "";
|
||||||
return (
|
return (
|
||||||
contactInfo +
|
contactInfo +
|
||||||
" possesses [" +
|
" possesses [" +
|
||||||
@@ -1242,11 +1298,7 @@ export const claimSpecialDescription = (
|
|||||||
"...]"
|
"...]"
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return issuer + " declared " + claimSummary(claim);
|
||||||
issuer +
|
|
||||||
" declared " +
|
|
||||||
claimSummary(claim as GenericCredWrapper<GenericVerifiableCredential>)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1286,32 +1338,42 @@ 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,
|
||||||
inviteId?: string,
|
identifier?: string,
|
||||||
expiresIn?: number,
|
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 };
|
||||||
}
|
}
|
||||||
if (inviteId) {
|
|
||||||
vcClaim.identifier = inviteId;
|
|
||||||
}
|
|
||||||
// Make a payload for the claim
|
// Make a payload for the claim
|
||||||
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, // Type assertion needed due to object being string
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create a signature using private key of identity
|
// Create a signature using private key of identity
|
||||||
const vcJwt = await createEndorserJwtForDid(activeDid, vcPayload, expiresIn);
|
const vcJwt = await createEndorserJwtForDid(activeDid, vcPayload, expiresIn);
|
||||||
return vcJwt;
|
return vcJwt;
|
||||||
@@ -1323,21 +1385,44 @@ export async function register(
|
|||||||
axios: Axios,
|
axios: Axios,
|
||||||
contact: Contact,
|
contact: Contact,
|
||||||
): Promise<{ success?: boolean; error?: string }> {
|
): Promise<{ success?: boolean; error?: string }> {
|
||||||
const vcJwt = await createInviteJwt(activeDid, contact);
|
try {
|
||||||
|
const vcJwt = await createInviteJwt(activeDid, contact);
|
||||||
|
const url = apiServer + "/api/v2/claim";
|
||||||
|
const resp = await axios.post<{
|
||||||
|
success?: {
|
||||||
|
handleId?: string;
|
||||||
|
embeddedRecordError?: string;
|
||||||
|
};
|
||||||
|
error?: string;
|
||||||
|
message?: string;
|
||||||
|
}>(url, { jwtEncoded: vcJwt });
|
||||||
|
|
||||||
const url = apiServer + "/api/v2/claim";
|
if (resp.data?.success?.handleId) {
|
||||||
const resp = await axios.post(url, { jwtEncoded: vcJwt });
|
return { success: true };
|
||||||
if (resp.data?.success?.handleId) {
|
} else if (resp.data?.success?.embeddedRecordError) {
|
||||||
return { success: true };
|
let message =
|
||||||
} else if (resp.data?.success?.embeddedRecordError) {
|
"There was some problem with the registration and so it may not be complete.";
|
||||||
let message =
|
if (typeof resp.data.success.embeddedRecordError === "string") {
|
||||||
"There was some problem with the registration and so it may not be complete.";
|
message += " " + resp.data.success.embeddedRecordError;
|
||||||
if (typeof resp.data.success.embeddedRecordError == "string") {
|
}
|
||||||
message += " " + resp.data.success.embeddedRecordError;
|
return { error: message };
|
||||||
|
} else {
|
||||||
|
logger.error("Registration error:", JSON.stringify(resp.data));
|
||||||
|
return { error: "Got a server error when registering." };
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error && typeof error === "object") {
|
||||||
|
const err = error as AxiosErrorResponse;
|
||||||
|
const errorMessage =
|
||||||
|
err.message ||
|
||||||
|
(err.response?.data &&
|
||||||
|
typeof err.response.data === "object" &&
|
||||||
|
"message" in err.response.data
|
||||||
|
? (err.response.data as { message: string }).message
|
||||||
|
: undefined);
|
||||||
|
logger.error("Registration error:", errorMessage || JSON.stringify(err));
|
||||||
|
return { error: errorMessage || "Got a server error when registering." };
|
||||||
}
|
}
|
||||||
return { error: message };
|
|
||||||
} else {
|
|
||||||
logger.error(resp);
|
|
||||||
return { error: "Got a server error when registering." };
|
return { error: "Got a server error when registering." };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1363,7 +1448,14 @@ export async function setVisibilityUtil(
|
|||||||
if (resp.status === 200) {
|
if (resp.status === 200) {
|
||||||
const success = resp.data.success;
|
const success = resp.data.success;
|
||||||
if (success) {
|
if (success) {
|
||||||
db.contacts.update(contact.did, { seesMe: visibility });
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
|
await platformService.dbExec(
|
||||||
|
"UPDATE contacts SET seesMe = ? WHERE did = ?",
|
||||||
|
[visibility, contact.did],
|
||||||
|
);
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
db.contacts.update(contact.did, { seesMe: visibility });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return { success };
|
return { success };
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
475
src/libs/util.ts
475
src/libs/util.ts
@@ -5,29 +5,45 @@ import { Buffer } from "buffer";
|
|||||||
import * as R from "ramda";
|
import * as R from "ramda";
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { useClipboard } from "@vueuse/core";
|
||||||
|
|
||||||
import { DEFAULT_PUSH_SERVER, NotificationIface } from "../constants/app";
|
import {
|
||||||
|
DEFAULT_PUSH_SERVER,
|
||||||
|
NotificationIface,
|
||||||
|
USE_DEXIE_DB,
|
||||||
|
} from "../constants/app";
|
||||||
import {
|
import {
|
||||||
accountsDBPromise,
|
accountsDBPromise,
|
||||||
retrieveSettingsForActiveAccount,
|
retrieveSettingsForActiveAccount,
|
||||||
updateAccountSettings,
|
updateAccountSettings,
|
||||||
updateDefaultSettings,
|
updateDefaultSettings,
|
||||||
} from "../db/index";
|
} from "../db/index";
|
||||||
import { Account } from "../db/tables/accounts";
|
import { Account, AccountEncrypted } from "../db/tables/accounts";
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from "../db/tables/contacts";
|
||||||
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
import { DEFAULT_PASSKEY_EXPIRATION_MINUTES } from "../db/tables/settings";
|
import { DEFAULT_PASSKEY_EXPIRATION_MINUTES } from "../db/tables/settings";
|
||||||
import { deriveAddress, generateSeed, newIdentifier } from "../libs/crypto";
|
|
||||||
import * as serverUtil from "../libs/endorserServer";
|
|
||||||
import {
|
import {
|
||||||
containsHiddenDid,
|
arrayBufferToBase64,
|
||||||
|
base64ToArrayBuffer,
|
||||||
|
deriveAddress,
|
||||||
|
generateSeed,
|
||||||
|
newIdentifier,
|
||||||
|
simpleDecrypt,
|
||||||
|
simpleEncrypt,
|
||||||
|
} from "../libs/crypto";
|
||||||
|
import * as serverUtil from "../libs/endorserServer";
|
||||||
|
import { containsHiddenDid } from "../libs/endorserServer";
|
||||||
|
import {
|
||||||
GenericCredWrapper,
|
GenericCredWrapper,
|
||||||
GenericVerifiableCredential,
|
GenericVerifiableCredential,
|
||||||
GiveSummaryRecord,
|
KeyMetaWithPrivate,
|
||||||
OfferVerifiableCredential,
|
} from "../interfaces/common";
|
||||||
} from "../libs/endorserServer";
|
import { GiveSummaryRecord } from "../interfaces/records";
|
||||||
import { KeyMeta } from "../libs/crypto/vc";
|
import { OfferClaim } from "../interfaces/claims";
|
||||||
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";
|
||||||
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||||
|
import { sha256 } from "ethereum-cryptography/sha256";
|
||||||
|
import { IIdentifier } from "@veramo/core";
|
||||||
|
|
||||||
export interface GiverReceiverInputInfo {
|
export interface GiverReceiverInputInfo {
|
||||||
did?: string;
|
did?: string;
|
||||||
@@ -66,18 +82,24 @@ export const UNIT_LONG: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
/* eslint-enable prettier/prettier */
|
/* eslint-enable prettier/prettier */
|
||||||
|
|
||||||
const UNIT_CODES: Record<string, Record<string, string>> = {
|
const UNIT_CODES: Record<
|
||||||
|
string,
|
||||||
|
{ name: string; faIcon: string; decimals: number }
|
||||||
|
> = {
|
||||||
BTC: {
|
BTC: {
|
||||||
name: "Bitcoin",
|
name: "Bitcoin",
|
||||||
faIcon: "bitcoin-sign",
|
faIcon: "bitcoin-sign",
|
||||||
|
decimals: 4,
|
||||||
},
|
},
|
||||||
HUR: {
|
HUR: {
|
||||||
name: "hours",
|
name: "hours",
|
||||||
faIcon: "clock",
|
faIcon: "clock",
|
||||||
|
decimals: 0,
|
||||||
},
|
},
|
||||||
USD: {
|
USD: {
|
||||||
name: "US Dollars",
|
name: "US Dollars",
|
||||||
faIcon: "dollar",
|
faIcon: "dollar",
|
||||||
|
decimals: 2,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -85,6 +107,13 @@ export function iconForUnitCode(unitCode: string) {
|
|||||||
return UNIT_CODES[unitCode]?.faIcon || "question";
|
return UNIT_CODES[unitCode]?.faIcon || "question";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function formattedAmount(amount: number, unitCode: string) {
|
||||||
|
const unit = UNIT_CODES[unitCode];
|
||||||
|
const amountStr = amount.toFixed(unit?.decimals ?? 4);
|
||||||
|
const unitName = unit?.name || "?";
|
||||||
|
return amountStr + " " + unitName;
|
||||||
|
}
|
||||||
|
|
||||||
// from https://stackoverflow.com/a/175787/845494
|
// from https://stackoverflow.com/a/175787/845494
|
||||||
// ... though it appears even this isn't precisely right so keep doing "|| 0" or something in sensitive places
|
// ... though it appears even this isn't precisely right so keep doing "|| 0" or something in sensitive places
|
||||||
//
|
//
|
||||||
@@ -364,16 +393,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<OfferVerifiableCredential>,
|
veriClaim: GenericCredWrapper<OfferClaim>,
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
let giver;
|
const innerClaim = veriClaim.claim as OfferClaim;
|
||||||
if (
|
let giver: string | undefined = undefined;
|
||||||
veriClaim.claim.offeredBy?.identifier &&
|
|
||||||
!serverUtil.isHiddenDid(veriClaim.claim.offeredBy.identifier as string)
|
giver = innerClaim.offeredBy?.identifier;
|
||||||
) {
|
if (giver && !serverUtil.isHiddenDid(giver)) {
|
||||||
giver = veriClaim.claim.offeredBy.identifier;
|
return giver;
|
||||||
} 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;
|
||||||
}
|
}
|
||||||
@@ -384,10 +416,12 @@ export function offerGiverDid(
|
|||||||
*/
|
*/
|
||||||
export const canFulfillOffer = (
|
export const canFulfillOffer = (
|
||||||
veriClaim: GenericCredWrapper<GenericVerifiableCredential>,
|
veriClaim: GenericCredWrapper<GenericVerifiableCredential>,
|
||||||
|
isRegistered: boolean,
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
|
isRegistered &&
|
||||||
veriClaim.claimType === "Offer" &&
|
veriClaim.claimType === "Offer" &&
|
||||||
!!offerGiverDid(veriClaim as GenericCredWrapper<OfferVerifiableCredential>)
|
!!offerGiverDid(veriClaim as GenericCredWrapper<OfferClaim>)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -457,74 +491,234 @@ export function findAllVisibleToDids(
|
|||||||
*
|
*
|
||||||
**/
|
**/
|
||||||
|
|
||||||
export interface AccountKeyInfo extends Account, KeyMeta {}
|
export type AccountKeyInfo = Account & KeyMetaWithPrivate;
|
||||||
|
|
||||||
export const retrieveAccountCount = async (): Promise<number> => {
|
export const retrieveAccountCount = async (): Promise<number> => {
|
||||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
let result = 0;
|
||||||
const accountsDB = await accountsDBPromise;
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
return await accountsDB.accounts.count();
|
const dbResult = await platformService.dbQuery(
|
||||||
|
`SELECT COUNT(*) FROM accounts`,
|
||||||
|
);
|
||||||
|
if (dbResult?.values?.[0]?.[0]) {
|
||||||
|
result = dbResult.values[0][0] as number;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||||
|
const accountsDB = await accountsDBPromise;
|
||||||
|
result = await accountsDB.accounts.count();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const retrieveAccountDids = async (): Promise<string[]> => {
|
export const retrieveAccountDids = async (): Promise<string[]> => {
|
||||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
const accountsDB = await accountsDBPromise;
|
const dbAccounts = await platformService.dbQuery(`SELECT did FROM accounts`);
|
||||||
const allAccounts = await accountsDB.accounts.toArray();
|
let allDids =
|
||||||
const allDids = allAccounts.map((acc) => acc.did);
|
databaseUtil
|
||||||
|
.mapQueryResultToValues(dbAccounts)
|
||||||
|
?.map((row) => row[0] as string) || [];
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
// this is the old way
|
||||||
|
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||||
|
const accountsDB = await accountsDBPromise;
|
||||||
|
const allAccounts = await accountsDB.accounts.toArray();
|
||||||
|
allDids = allAccounts.map((acc) => acc.did);
|
||||||
|
}
|
||||||
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> => {
|
||||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
let result: Account | undefined = undefined;
|
||||||
const accountsDB = await accountsDBPromise;
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
const account = (await accountsDB.accounts
|
const dbAccount = await platformService.dbQuery(
|
||||||
.where("did")
|
`SELECT * FROM accounts WHERE did = ?`,
|
||||||
.equals(activeDid)
|
[activeDid],
|
||||||
.first()) as Account;
|
);
|
||||||
|
const account = databaseUtil.mapQueryResultToValues(dbAccount)[0] as Account;
|
||||||
if (account) {
|
if (account) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { identity, mnemonic, ...metadata } = account;
|
const { identity, mnemonic, ...metadata } = account;
|
||||||
return metadata;
|
result = metadata;
|
||||||
} else {
|
} else {
|
||||||
return undefined;
|
result = undefined;
|
||||||
}
|
}
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||||
|
const accountsDB = await accountsDBPromise;
|
||||||
|
const account = (await accountsDB.accounts
|
||||||
|
.where("did")
|
||||||
|
.equals(activeDid)
|
||||||
|
.first()) as Account;
|
||||||
|
if (account) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { identity, mnemonic, ...metadata } = account;
|
||||||
|
result = metadata;
|
||||||
|
} else {
|
||||||
|
result = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const retrieveAllAccountsMetadata = async (): Promise<Account[]> => {
|
/**
|
||||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
* This contains sensitive data. If possible, use retrieveAccountMetadata instead.
|
||||||
const accountsDB = await accountsDBPromise;
|
*
|
||||||
const array = await accountsDB.accounts.toArray();
|
* @param activeDid
|
||||||
return array.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;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const retrieveFullyDecryptedAccount = async (
|
export const retrieveFullyDecryptedAccount = async (
|
||||||
activeDid: string,
|
activeDid: string,
|
||||||
): Promise<AccountKeyInfo | undefined> => {
|
): Promise<Account | undefined> => {
|
||||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
let result: Account | undefined = undefined;
|
||||||
const accountsDB = await accountsDBPromise;
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
const account = (await accountsDB.accounts
|
const dbSecrets = await platformService.dbQuery(
|
||||||
.where("did")
|
`SELECT secretBase64 from secret`,
|
||||||
.equals(activeDid)
|
);
|
||||||
.first()) as Account;
|
if (
|
||||||
return account;
|
!dbSecrets ||
|
||||||
|
dbSecrets.values.length === 0 ||
|
||||||
|
dbSecrets.values[0].length === 0
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"No secret found. We recommend you clear your data and start over.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const secretBase64 = dbSecrets.values[0][0] as string;
|
||||||
|
const secret = base64ToArrayBuffer(secretBase64);
|
||||||
|
const dbAccount = await platformService.dbQuery(
|
||||||
|
`SELECT * FROM accounts WHERE did = ?`,
|
||||||
|
[activeDid],
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
!dbAccount ||
|
||||||
|
dbAccount.values.length === 0 ||
|
||||||
|
dbAccount.values[0].length === 0
|
||||||
|
) {
|
||||||
|
throw new Error("Account not found.");
|
||||||
|
}
|
||||||
|
const fullAccountData = databaseUtil.mapQueryResultToValues(
|
||||||
|
dbAccount,
|
||||||
|
)[0] as AccountEncrypted;
|
||||||
|
const identityEncr = base64ToArrayBuffer(fullAccountData.identityEncrBase64);
|
||||||
|
const mnemonicEncr = base64ToArrayBuffer(fullAccountData.mnemonicEncrBase64);
|
||||||
|
fullAccountData.identity = await simpleDecrypt(identityEncr, secret);
|
||||||
|
fullAccountData.mnemonic = await simpleDecrypt(mnemonicEncr, secret);
|
||||||
|
result = fullAccountData;
|
||||||
|
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||||
|
const accountsDB = await accountsDBPromise;
|
||||||
|
const account = (await accountsDB.accounts
|
||||||
|
.where("did")
|
||||||
|
.equals(activeDid)
|
||||||
|
.first()) as Account;
|
||||||
|
result = account;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
// let's try and eliminate this
|
export const retrieveAllAccountsMetadata = async (): Promise<
|
||||||
export const retrieveAllFullyDecryptedAccounts = async (): Promise<
|
AccountEncrypted[]
|
||||||
Array<AccountKeyInfo>
|
|
||||||
> => {
|
> => {
|
||||||
const accountsDB = await accountsDBPromise;
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
const allAccounts = await accountsDB.accounts.toArray();
|
const dbAccounts = await platformService.dbQuery(`SELECT * FROM accounts`);
|
||||||
return allAccounts;
|
const accounts = databaseUtil.mapQueryResultToValues(dbAccounts) as Account[];
|
||||||
|
let result = accounts.map((account) => {
|
||||||
|
return account as AccountEncrypted;
|
||||||
|
});
|
||||||
|
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;
|
||||||
|
// This is not accurate because they can't be decrypted, but we're removing Dexie anyway.
|
||||||
|
const identityStr = JSON.stringify(identity);
|
||||||
|
const encryptedAccount = {
|
||||||
|
identityEncrBase64: sha256(
|
||||||
|
new TextEncoder().encode(identityStr),
|
||||||
|
).toString(),
|
||||||
|
mnemonicEncrBase64: sha256(
|
||||||
|
new TextEncoder().encode(account.mnemonic),
|
||||||
|
).toString(),
|
||||||
|
...metadata,
|
||||||
|
};
|
||||||
|
return encryptedAccount as AccountEncrypted;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves a new identity to both SQL and Dexie databases
|
||||||
|
*/
|
||||||
|
export async function saveNewIdentity(
|
||||||
|
identity: IIdentifier,
|
||||||
|
mnemonic: string,
|
||||||
|
derivationPath: string,
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
// add to the new sql db
|
||||||
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
|
const secrets = await platformService.dbQuery(
|
||||||
|
`SELECT secretBase64 FROM secret`,
|
||||||
|
);
|
||||||
|
if (!secrets?.values?.length || !secrets.values[0]?.length) {
|
||||||
|
throw new Error(
|
||||||
|
"No initial encryption supported. We recommend you clear your data and start over.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const secretBase64 = secrets.values[0][0] as string;
|
||||||
|
const secret = base64ToArrayBuffer(secretBase64);
|
||||||
|
const identityStr = JSON.stringify(identity);
|
||||||
|
const encryptedIdentity = await simpleEncrypt(identityStr, secret);
|
||||||
|
const encryptedMnemonic = await simpleEncrypt(mnemonic, secret);
|
||||||
|
const encryptedIdentityBase64 = arrayBufferToBase64(encryptedIdentity);
|
||||||
|
const encryptedMnemonicBase64 = arrayBufferToBase64(encryptedMnemonic);
|
||||||
|
const sql = `INSERT INTO accounts (dateCreated, derivationPath, did, identityEncrBase64, mnemonicEncrBase64, publicKeyHex)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)`;
|
||||||
|
const params = [
|
||||||
|
new Date().toISOString(),
|
||||||
|
derivationPath,
|
||||||
|
identity.did,
|
||||||
|
encryptedIdentityBase64,
|
||||||
|
encryptedMnemonicBase64,
|
||||||
|
identity.keys[0].publicKeyHex,
|
||||||
|
];
|
||||||
|
await platformService.dbExec(sql, params);
|
||||||
|
await databaseUtil.updateDefaultSettings({ activeDid: identity.did });
|
||||||
|
|
||||||
|
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.add({
|
||||||
|
dateCreated: new Date().toISOString(),
|
||||||
|
derivationPath: derivationPath,
|
||||||
|
did: identity.did,
|
||||||
|
identity: identityStr,
|
||||||
|
mnemonic: mnemonic,
|
||||||
|
publicKeyHex: identity.keys[0].publicKeyHex,
|
||||||
|
});
|
||||||
|
await updateDefaultSettings({ activeDid: identity.did });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to update default settings:", error);
|
||||||
|
throw new Error(
|
||||||
|
"Failed to set default settings. Please try again or restart the app.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a new identity, saves it to the database, and sets it as the active identity.
|
* Generates a new identity, saves it to the database, and sets it as the active identity.
|
||||||
* @return {Promise<string>} with the DID of the new identity
|
* @return {Promise<string>} with the DID of the new identity
|
||||||
@@ -536,28 +730,12 @@ export const generateSaveAndActivateIdentity = async (): Promise<string> => {
|
|||||||
deriveAddress(mnemonic);
|
deriveAddress(mnemonic);
|
||||||
|
|
||||||
const newId = newIdentifier(address, publicHex, privateHex, derivationPath);
|
const newId = newIdentifier(address, publicHex, privateHex, derivationPath);
|
||||||
const identity = JSON.stringify(newId);
|
|
||||||
|
|
||||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
await saveNewIdentity(newId, mnemonic, derivationPath);
|
||||||
try {
|
await databaseUtil.updateAccountSettings(newId.did, { isRegistered: false });
|
||||||
const accountsDB = await accountsDBPromise;
|
if (USE_DEXIE_DB) {
|
||||||
await accountsDB.accounts.add({
|
await updateAccountSettings(newId.did, { isRegistered: false });
|
||||||
dateCreated: new Date().toISOString(),
|
|
||||||
derivationPath: derivationPath,
|
|
||||||
did: newId.did,
|
|
||||||
identity: identity,
|
|
||||||
mnemonic: mnemonic,
|
|
||||||
publicKeyHex: newId.keys[0].publicKeyHex,
|
|
||||||
});
|
|
||||||
|
|
||||||
await updateDefaultSettings({ activeDid: newId.did });
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to update default settings:", error);
|
|
||||||
throw new Error(
|
|
||||||
"Failed to set default settings. Please try again or restart the app.",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
await updateAccountSettings(newId.did, { isRegistered: false });
|
|
||||||
return newId.did;
|
return newId.did;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -575,9 +753,19 @@ export const registerAndSavePasskey = async (
|
|||||||
passkeyCredIdHex,
|
passkeyCredIdHex,
|
||||||
publicKeyHex: Buffer.from(publicKeyBytes).toString("hex"),
|
publicKeyHex: Buffer.from(publicKeyBytes).toString("hex"),
|
||||||
};
|
};
|
||||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
const insertStatement = databaseUtil.generateInsertStatement(
|
||||||
const accountsDB = await accountsDBPromise;
|
account,
|
||||||
await accountsDB.accounts.add(account);
|
"accounts",
|
||||||
|
);
|
||||||
|
await PlatformServiceFactory.getInstance().dbExec(
|
||||||
|
insertStatement.sql,
|
||||||
|
insertStatement.params,
|
||||||
|
);
|
||||||
|
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.add(account);
|
||||||
|
}
|
||||||
return account;
|
return account;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -585,13 +773,22 @@ export const registerSaveAndActivatePasskey = async (
|
|||||||
keyName: string,
|
keyName: string,
|
||||||
): Promise<Account> => {
|
): Promise<Account> => {
|
||||||
const account = await registerAndSavePasskey(keyName);
|
const account = await registerAndSavePasskey(keyName);
|
||||||
await updateDefaultSettings({ activeDid: account.did });
|
await databaseUtil.updateDefaultSettings({ activeDid: account.did });
|
||||||
await updateAccountSettings(account.did, { isRegistered: false });
|
await databaseUtil.updateAccountSettings(account.did, {
|
||||||
|
isRegistered: false,
|
||||||
|
});
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
await updateDefaultSettings({ activeDid: account.did });
|
||||||
|
await updateAccountSettings(account.did, { isRegistered: false });
|
||||||
|
}
|
||||||
return account;
|
return account;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getPasskeyExpirationSeconds = async (): Promise<number> => {
|
export const getPasskeyExpirationSeconds = async (): Promise<number> => {
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
settings = await retrieveSettingsForActiveAccount();
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
(settings?.passkeyExpirationMinutes ?? DEFAULT_PASSKEY_EXPIRATION_MINUTES) *
|
(settings?.passkeyExpirationMinutes ?? DEFAULT_PASSKEY_EXPIRATION_MINUTES) *
|
||||||
60
|
60
|
||||||
@@ -607,7 +804,10 @@ export const sendTestThroughPushServer = async (
|
|||||||
subscriptionJSON: PushSubscriptionJSON,
|
subscriptionJSON: PushSubscriptionJSON,
|
||||||
skipFilter: boolean,
|
skipFilter: boolean,
|
||||||
): Promise<AxiosResponse> => {
|
): Promise<AxiosResponse> => {
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
settings = await retrieveSettingsForActiveAccount();
|
||||||
|
}
|
||||||
let pushUrl: string = DEFAULT_PUSH_SERVER as string;
|
let pushUrl: string = DEFAULT_PUSH_SERVER as string;
|
||||||
if (settings?.webPushServer) {
|
if (settings?.webPushServer) {
|
||||||
pushUrl = settings.webPushServer;
|
pushUrl = settings.webPushServer;
|
||||||
@@ -635,3 +835,96 @@ export const sendTestThroughPushServer = async (
|
|||||||
logger.log("Got response from web push server:", response);
|
logger.log("Got response from web push server:", response);
|
||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Contact object to a CSV line string following the established format.
|
||||||
|
* The format matches CONTACT_CSV_HEADER: "name,did,pubKeyBase64,seesMe,registered,contactMethods"
|
||||||
|
* where contactMethods is stored as a stringified JSON array.
|
||||||
|
*
|
||||||
|
* @param contact - The Contact object to convert
|
||||||
|
* @returns A CSV-formatted string representing the contact
|
||||||
|
* @throws {Error} If the contact object is missing required fields
|
||||||
|
*/
|
||||||
|
export const contactToCsvLine = (contact: Contact): string => {
|
||||||
|
if (!contact.did) {
|
||||||
|
throw new Error("Contact must have a did field");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape fields that might contain commas or quotes
|
||||||
|
const escapeField = (field: string | boolean | undefined): string => {
|
||||||
|
if (field === undefined) return "";
|
||||||
|
const str = String(field);
|
||||||
|
if (str.includes(",") || str.includes('"')) {
|
||||||
|
return `"${str.replace(/"/g, '""')}"`;
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle contactMethods array by stringifying it
|
||||||
|
const contactMethodsStr = contact.contactMethods
|
||||||
|
? escapeField(JSON.stringify(contact.contactMethods))
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const fields = [
|
||||||
|
escapeField(contact.name),
|
||||||
|
escapeField(contact.did),
|
||||||
|
escapeField(contact.publicKeyBase64),
|
||||||
|
escapeField(contact.seesMe),
|
||||||
|
escapeField(contact.registered),
|
||||||
|
contactMethodsStr,
|
||||||
|
];
|
||||||
|
|
||||||
|
return fields.join(",");
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for the JSON export format of database tables
|
||||||
|
*/
|
||||||
|
export interface TableExportData {
|
||||||
|
tableName: string;
|
||||||
|
rows: Array<Record<string, unknown>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for the complete database export format
|
||||||
|
*/
|
||||||
|
export interface DatabaseExport {
|
||||||
|
data: {
|
||||||
|
data: Array<TableExportData>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an array of contacts to the standardized database export JSON format.
|
||||||
|
* This format is used for data migration and backup purposes.
|
||||||
|
*
|
||||||
|
* @param contacts - Array of Contact objects to convert
|
||||||
|
* @returns DatabaseExport object in the standardized format
|
||||||
|
*/
|
||||||
|
export const contactsToExportJson = (contacts: Contact[]): DatabaseExport => {
|
||||||
|
// Convert each contact to a plain object and ensure all fields are included
|
||||||
|
const rows = contacts.map((contact) => ({
|
||||||
|
did: contact.did,
|
||||||
|
name: contact.name || null,
|
||||||
|
contactMethods: contact.contactMethods
|
||||||
|
? JSON.stringify(contact.contactMethods)
|
||||||
|
: null,
|
||||||
|
nextPubKeyHashB64: contact.nextPubKeyHashB64 || null,
|
||||||
|
notes: contact.notes || null,
|
||||||
|
profileImageUrl: contact.profileImageUrl || null,
|
||||||
|
publicKeyBase64: contact.publicKeyBase64 || null,
|
||||||
|
seesMe: contact.seesMe || false,
|
||||||
|
registered: contact.registered || false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
tableName: "contacts",
|
||||||
|
rows,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import router from "./router";
|
|||||||
import { handleApiError } from "./services/api";
|
import { handleApiError } from "./services/api";
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import { DeepLinkHandler } from "./services/deepLinks";
|
import { DeepLinkHandler } from "./services/deepLinks";
|
||||||
import { logConsoleAndDb } from "./db";
|
import { logConsoleAndDb } from "./db/databaseUtil";
|
||||||
import { logger } from "./utils/logger";
|
import { logger } from "./utils/logger";
|
||||||
|
|
||||||
logger.log("[Capacitor] Starting initialization");
|
logger.log("[Capacitor] Starting initialization");
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ import { FontAwesomeIcon } from "./libs/fontawesome";
|
|||||||
import Camera from "simple-vue-camera";
|
import Camera from "simple-vue-camera";
|
||||||
import { logger } from "./utils/logger";
|
import { logger } from "./utils/logger";
|
||||||
|
|
||||||
|
const platform = process.env.VITE_PLATFORM;
|
||||||
|
const pwa_enabled = process.env.VITE_PWA_ENABLED === "true";
|
||||||
|
|
||||||
|
logger.log("Platform", JSON.stringify({ platform }));
|
||||||
|
logger.log("PWA enabled", JSON.stringify({ pwa_enabled }));
|
||||||
|
|
||||||
// Global Error Handler
|
// Global Error Handler
|
||||||
function setupGlobalErrorHandler(app: VueApp) {
|
function setupGlobalErrorHandler(app: VueApp) {
|
||||||
logger.log("[App Init] Setting up global error handler");
|
logger.log("[App Init] Setting up global error handler");
|
||||||
|
|||||||
@@ -1,4 +1,16 @@
|
|||||||
import { initializeApp } from "./main.common";
|
import { initializeApp } from "./main.common";
|
||||||
|
import { logger } from "./utils/logger";
|
||||||
|
|
||||||
|
const platform = process.env.VITE_PLATFORM;
|
||||||
|
const pwa_enabled = process.env.VITE_PWA_ENABLED === "true";
|
||||||
|
|
||||||
|
logger.info("[Electron] Initializing app");
|
||||||
|
logger.info("[Electron] Platform:", { platform });
|
||||||
|
logger.info("[Electron] PWA enabled:", { pwa_enabled });
|
||||||
|
|
||||||
|
if (pwa_enabled) {
|
||||||
|
logger.warn("[Electron] PWA is enabled, but not supported in electron");
|
||||||
|
}
|
||||||
|
|
||||||
const app = initializeApp();
|
const app = initializeApp();
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
|
|||||||
215
src/main.ts
215
src/main.ts
@@ -1,215 +0,0 @@
|
|||||||
import { createPinia } from "pinia";
|
|
||||||
import { App as VueApp, ComponentPublicInstance, createApp } from "vue";
|
|
||||||
import App from "./App.vue";
|
|
||||||
import "./registerServiceWorker";
|
|
||||||
import router from "./router";
|
|
||||||
import axios from "axios";
|
|
||||||
import VueAxios from "vue-axios";
|
|
||||||
import Notifications from "notiwind";
|
|
||||||
import "./assets/styles/tailwind.css";
|
|
||||||
|
|
||||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
|
||||||
import {
|
|
||||||
faArrowDown,
|
|
||||||
faArrowLeft,
|
|
||||||
faArrowRight,
|
|
||||||
faArrowRotateBackward,
|
|
||||||
faArrowUpRightFromSquare,
|
|
||||||
faArrowUp,
|
|
||||||
faBan,
|
|
||||||
faBitcoinSign,
|
|
||||||
faBurst,
|
|
||||||
faCalendar,
|
|
||||||
faCamera,
|
|
||||||
faCameraRotate,
|
|
||||||
faCaretDown,
|
|
||||||
faChair,
|
|
||||||
faCheck,
|
|
||||||
faChevronDown,
|
|
||||||
faChevronLeft,
|
|
||||||
faChevronRight,
|
|
||||||
faChevronUp,
|
|
||||||
faCircle,
|
|
||||||
faCircleCheck,
|
|
||||||
faCircleInfo,
|
|
||||||
faCircleQuestion,
|
|
||||||
faCircleUser,
|
|
||||||
faClock,
|
|
||||||
faCoins,
|
|
||||||
faComment,
|
|
||||||
faCopy,
|
|
||||||
faDollar,
|
|
||||||
faEllipsis,
|
|
||||||
faEllipsisVertical,
|
|
||||||
faEnvelopeOpenText,
|
|
||||||
faEraser,
|
|
||||||
faEye,
|
|
||||||
faEyeSlash,
|
|
||||||
faFileContract,
|
|
||||||
faFileLines,
|
|
||||||
faFilter,
|
|
||||||
faFloppyDisk,
|
|
||||||
faFolderOpen,
|
|
||||||
faForward,
|
|
||||||
faGift,
|
|
||||||
faGlobe,
|
|
||||||
faHammer,
|
|
||||||
faHand,
|
|
||||||
faHandHoldingDollar,
|
|
||||||
faHandHoldingHeart,
|
|
||||||
faHouseChimney,
|
|
||||||
faImage,
|
|
||||||
faImagePortrait,
|
|
||||||
faLeftRight,
|
|
||||||
faLightbulb,
|
|
||||||
faLink,
|
|
||||||
faLocationDot,
|
|
||||||
faLongArrowAltLeft,
|
|
||||||
faLongArrowAltRight,
|
|
||||||
faMagnifyingGlass,
|
|
||||||
faMessage,
|
|
||||||
faMinus,
|
|
||||||
faPen,
|
|
||||||
faPersonCircleCheck,
|
|
||||||
faPersonCircleQuestion,
|
|
||||||
faPlus,
|
|
||||||
faQuestion,
|
|
||||||
faQrcode,
|
|
||||||
faRightFromBracket,
|
|
||||||
faRotate,
|
|
||||||
faShareNodes,
|
|
||||||
faSpinner,
|
|
||||||
faSquare,
|
|
||||||
faSquareCaretDown,
|
|
||||||
faSquareCaretUp,
|
|
||||||
faSquarePlus,
|
|
||||||
faTrashCan,
|
|
||||||
faTriangleExclamation,
|
|
||||||
faUser,
|
|
||||||
faUsers,
|
|
||||||
faXmark,
|
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
|
||||||
|
|
||||||
library.add(
|
|
||||||
faArrowDown,
|
|
||||||
faArrowLeft,
|
|
||||||
faArrowRight,
|
|
||||||
faArrowRotateBackward,
|
|
||||||
faArrowUpRightFromSquare,
|
|
||||||
faArrowUp,
|
|
||||||
faBan,
|
|
||||||
faBitcoinSign,
|
|
||||||
faBurst,
|
|
||||||
faCalendar,
|
|
||||||
faCamera,
|
|
||||||
faCameraRotate,
|
|
||||||
faCaretDown,
|
|
||||||
faChair,
|
|
||||||
faCheck,
|
|
||||||
faChevronDown,
|
|
||||||
faChevronLeft,
|
|
||||||
faChevronRight,
|
|
||||||
faChevronUp,
|
|
||||||
faCircle,
|
|
||||||
faCircleCheck,
|
|
||||||
faCircleInfo,
|
|
||||||
faCircleQuestion,
|
|
||||||
faCircleUser,
|
|
||||||
faClock,
|
|
||||||
faCoins,
|
|
||||||
faComment,
|
|
||||||
faCopy,
|
|
||||||
faDollar,
|
|
||||||
faEllipsis,
|
|
||||||
faEllipsisVertical,
|
|
||||||
faEnvelopeOpenText,
|
|
||||||
faEraser,
|
|
||||||
faEye,
|
|
||||||
faEyeSlash,
|
|
||||||
faFileContract,
|
|
||||||
faFileLines,
|
|
||||||
faFilter,
|
|
||||||
faFloppyDisk,
|
|
||||||
faFolderOpen,
|
|
||||||
faForward,
|
|
||||||
faGift,
|
|
||||||
faGlobe,
|
|
||||||
faHammer,
|
|
||||||
faHand,
|
|
||||||
faHandHoldingDollar,
|
|
||||||
faHandHoldingHeart,
|
|
||||||
faHouseChimney,
|
|
||||||
faImage,
|
|
||||||
faImagePortrait,
|
|
||||||
faLeftRight,
|
|
||||||
faLightbulb,
|
|
||||||
faLink,
|
|
||||||
faLocationDot,
|
|
||||||
faLongArrowAltLeft,
|
|
||||||
faLongArrowAltRight,
|
|
||||||
faMagnifyingGlass,
|
|
||||||
faMessage,
|
|
||||||
faMinus,
|
|
||||||
faPen,
|
|
||||||
faPersonCircleCheck,
|
|
||||||
faPersonCircleQuestion,
|
|
||||||
faPlus,
|
|
||||||
faQrcode,
|
|
||||||
faQuestion,
|
|
||||||
faRotate,
|
|
||||||
faRightFromBracket,
|
|
||||||
faShareNodes,
|
|
||||||
faSpinner,
|
|
||||||
faSquare,
|
|
||||||
faSquareCaretDown,
|
|
||||||
faSquareCaretUp,
|
|
||||||
faSquarePlus,
|
|
||||||
faTrashCan,
|
|
||||||
faTriangleExclamation,
|
|
||||||
faUser,
|
|
||||||
faUsers,
|
|
||||||
faXmark,
|
|
||||||
);
|
|
||||||
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
|
||||||
import Camera from "simple-vue-camera";
|
|
||||||
import { logger } from "./utils/logger";
|
|
||||||
|
|
||||||
// Can trigger this with a 'throw' inside some top-level function, eg. on the HomeView
|
|
||||||
function setupGlobalErrorHandler(app: VueApp) {
|
|
||||||
// @ts-expect-error 'cause we cannot see why config is not defined
|
|
||||||
app.config.errorHandler = (
|
|
||||||
err: Error,
|
|
||||||
instance: ComponentPublicInstance | null,
|
|
||||||
info: string,
|
|
||||||
) => {
|
|
||||||
logger.error(
|
|
||||||
"Ouch! Global Error Handler.",
|
|
||||||
"Error:",
|
|
||||||
err,
|
|
||||||
"- Error toString:",
|
|
||||||
err.toString(),
|
|
||||||
"- Info:",
|
|
||||||
info,
|
|
||||||
"- Instance:",
|
|
||||||
instance,
|
|
||||||
);
|
|
||||||
// Want to show a nice notiwind notification but can't figure out how.
|
|
||||||
alert(
|
|
||||||
(err.message || "Something bad happened") +
|
|
||||||
" - Try reloading or restarting the app.",
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const app = createApp(App)
|
|
||||||
.component("fa", FontAwesomeIcon)
|
|
||||||
.component("camera", Camera)
|
|
||||||
.use(createPinia())
|
|
||||||
.use(VueAxios, axios)
|
|
||||||
.use(router)
|
|
||||||
.use(Notifications);
|
|
||||||
|
|
||||||
setupGlobalErrorHandler(app);
|
|
||||||
|
|
||||||
app.mount("#app");
|
|
||||||
@@ -1,5 +1,37 @@
|
|||||||
|
import { initBackend } from "absurd-sql/dist/indexeddb-main-thread";
|
||||||
import { initializeApp } from "./main.common";
|
import { initializeApp } from "./main.common";
|
||||||
import "./registerServiceWorker"; // Web PWA support
|
import { logger } from "./utils/logger";
|
||||||
|
|
||||||
|
const platform = process.env.VITE_PLATFORM;
|
||||||
|
const pwa_enabled = process.env.VITE_PWA_ENABLED === "true";
|
||||||
|
|
||||||
|
logger.info("[Web] PWA enabled", { pwa_enabled });
|
||||||
|
logger.info("[Web] Platform", { platform });
|
||||||
|
|
||||||
|
// Only import service worker for web builds
|
||||||
|
if (platform !== "electron" && pwa_enabled) {
|
||||||
|
import("./registerServiceWorker"); // Web PWA support
|
||||||
|
}
|
||||||
|
|
||||||
const app = initializeApp();
|
const app = initializeApp();
|
||||||
|
|
||||||
|
function sqlInit() {
|
||||||
|
// see https://github.com/jlongster/absurd-sql
|
||||||
|
const worker = new Worker(
|
||||||
|
new URL("./registerSQLWorker.js", import.meta.url),
|
||||||
|
{
|
||||||
|
type: "module",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// This is only required because Safari doesn't support nested
|
||||||
|
// workers. This installs a handler that will proxy creating web
|
||||||
|
// workers through the main thread
|
||||||
|
initBackend(worker);
|
||||||
|
}
|
||||||
|
if (platform === "web" || platform === "development") {
|
||||||
|
sqlInit();
|
||||||
|
} else {
|
||||||
|
logger.info("[Web] SQL not initialized for platform", { platform });
|
||||||
|
}
|
||||||
|
|
||||||
app.mount("#app");
|
app.mount("#app");
|
||||||
|
|||||||
6
src/registerSQLWorker.js
Normal file
6
src/registerSQLWorker.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import databaseService from "./services/AbsurdSqlDatabaseService";
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
await databaseService.initialize();
|
||||||
|
}
|
||||||
|
run();
|
||||||
@@ -2,8 +2,18 @@
|
|||||||
|
|
||||||
import { register } from "register-service-worker";
|
import { register } from "register-service-worker";
|
||||||
|
|
||||||
// Only register service worker if explicitly enabled and in production
|
// Check if we're in an Electron environment
|
||||||
|
const isElectron =
|
||||||
|
process.env.VITE_PLATFORM === "electron" ||
|
||||||
|
process.env.VITE_DISABLE_PWA === "true" ||
|
||||||
|
window.navigator.userAgent.toLowerCase().includes("electron");
|
||||||
|
|
||||||
|
// Only register service worker if:
|
||||||
|
// 1. Not in Electron
|
||||||
|
// 2. PWA is explicitly enabled
|
||||||
|
// 3. In production mode
|
||||||
if (
|
if (
|
||||||
|
!isElectron &&
|
||||||
process.env.VITE_PWA_ENABLED === "true" &&
|
process.env.VITE_PWA_ENABLED === "true" &&
|
||||||
process.env.NODE_ENV === "production"
|
process.env.NODE_ENV === "production"
|
||||||
) {
|
) {
|
||||||
@@ -34,6 +44,12 @@ if (
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(
|
||||||
"Service worker registration skipped - not enabled or not in production",
|
`Service worker registration skipped - ${
|
||||||
|
isElectron
|
||||||
|
? "running in Electron"
|
||||||
|
: process.env.VITE_PWA_ENABLED !== "true"
|
||||||
|
? "PWA not enabled"
|
||||||
|
: "not in production mode"
|
||||||
|
}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,35 +2,11 @@ import {
|
|||||||
createRouter,
|
createRouter,
|
||||||
createWebHistory,
|
createWebHistory,
|
||||||
createMemoryHistory,
|
createMemoryHistory,
|
||||||
NavigationGuardNext,
|
|
||||||
RouteLocationNormalized,
|
RouteLocationNormalized,
|
||||||
RouteRecordRaw,
|
RouteRecordRaw,
|
||||||
} from "vue-router";
|
} from "vue-router";
|
||||||
import { accountsDBPromise } from "../db/index";
|
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param to :RouteLocationNormalized
|
|
||||||
* @param from :RouteLocationNormalized
|
|
||||||
* @param next :NavigationGuardNext
|
|
||||||
*/
|
|
||||||
const enterOrStart = async (
|
|
||||||
to: RouteLocationNormalized,
|
|
||||||
from: RouteLocationNormalized,
|
|
||||||
next: NavigationGuardNext,
|
|
||||||
) => {
|
|
||||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
|
||||||
const accountsDB = await accountsDBPromise;
|
|
||||||
const num_accounts = await accountsDB.accounts.count();
|
|
||||||
|
|
||||||
if (num_accounts > 0) {
|
|
||||||
next();
|
|
||||||
} else {
|
|
||||||
next({ name: "start" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const routes: Array<RouteRecordRaw> = [
|
const routes: Array<RouteRecordRaw> = [
|
||||||
{
|
{
|
||||||
path: "/account",
|
path: "/account",
|
||||||
@@ -216,7 +192,6 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
path: "/projects",
|
path: "/projects",
|
||||||
name: "projects",
|
name: "projects",
|
||||||
component: () => import("../views/ProjectsView.vue"),
|
component: () => import("../views/ProjectsView.vue"),
|
||||||
beforeEnter: enterOrStart,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/quick-action-bvc",
|
path: "/quick-action-bvc",
|
||||||
|
|||||||
29
src/services/AbsurdSqlDatabaseService.d.ts
vendored
Normal file
29
src/services/AbsurdSqlDatabaseService.d.ts
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { DatabaseService } from "../interfaces/database";
|
||||||
|
|
||||||
|
declare module "@jlongster/sql.js" {
|
||||||
|
interface SQL {
|
||||||
|
Database: unknown;
|
||||||
|
FS: unknown;
|
||||||
|
register_for_idb: (fs: unknown) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initSqlJs(config: {
|
||||||
|
locateFile: (file: string) => string;
|
||||||
|
}): Promise<SQL>;
|
||||||
|
export default initSqlJs;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "absurd-sql" {
|
||||||
|
export class SQLiteFS {
|
||||||
|
constructor(fs: unknown, backend: unknown);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module "absurd-sql/dist/indexeddb-backend" {
|
||||||
|
export default class IndexedDBBackend {
|
||||||
|
constructor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare const databaseService: DatabaseService;
|
||||||
|
export default databaseService;
|
||||||
231
src/services/AbsurdSqlDatabaseService.ts
Normal file
231
src/services/AbsurdSqlDatabaseService.ts
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
import initSqlJs from "@jlongster/sql.js";
|
||||||
|
import { SQLiteFS } from "absurd-sql";
|
||||||
|
import IndexedDBBackend from "absurd-sql/dist/indexeddb-backend";
|
||||||
|
|
||||||
|
import { runMigrations } from "../db-sql/migration";
|
||||||
|
import type { DatabaseService, QueryExecResult } from "../interfaces/database";
|
||||||
|
import { logger } from "@/utils/logger";
|
||||||
|
|
||||||
|
interface QueuedOperation {
|
||||||
|
type: "run" | "query";
|
||||||
|
sql: string;
|
||||||
|
params: unknown[];
|
||||||
|
resolve: (value: unknown) => void;
|
||||||
|
reject: (reason: unknown) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AbsurdSqlDatabase {
|
||||||
|
exec: (sql: string, params?: unknown[]) => Promise<QueryExecResult[]>;
|
||||||
|
run: (
|
||||||
|
sql: string,
|
||||||
|
params?: unknown[],
|
||||||
|
) => Promise<{ changes: number; lastId?: number }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AbsurdSqlDatabaseService implements DatabaseService {
|
||||||
|
private static instance: AbsurdSqlDatabaseService | null = null;
|
||||||
|
private db: AbsurdSqlDatabase | null;
|
||||||
|
private initialized: boolean;
|
||||||
|
private initializationPromise: Promise<void> | null = null;
|
||||||
|
private operationQueue: Array<QueuedOperation> = [];
|
||||||
|
private isProcessingQueue: boolean = false;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.db = null;
|
||||||
|
this.initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getInstance(): AbsurdSqlDatabaseService {
|
||||||
|
if (!AbsurdSqlDatabaseService.instance) {
|
||||||
|
AbsurdSqlDatabaseService.instance = new AbsurdSqlDatabaseService();
|
||||||
|
}
|
||||||
|
return AbsurdSqlDatabaseService.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize(): Promise<void> {
|
||||||
|
// If already initialized, return immediately
|
||||||
|
if (this.initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If initialization is in progress, wait for it
|
||||||
|
if (this.initializationPromise) {
|
||||||
|
return this.initializationPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start initialization
|
||||||
|
this.initializationPromise = this._initialize();
|
||||||
|
try {
|
||||||
|
await this.initializationPromise;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`AbsurdSqlDatabaseService initialize method failed:`, error);
|
||||||
|
this.initializationPromise = null; // Reset on failure
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _initialize(): Promise<void> {
|
||||||
|
if (this.initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SQL = await initSqlJs({
|
||||||
|
locateFile: (file: string) => {
|
||||||
|
return new URL(
|
||||||
|
`/node_modules/@jlongster/sql.js/dist/${file}`,
|
||||||
|
import.meta.url,
|
||||||
|
).href;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const sqlFS = new SQLiteFS(SQL.FS, new IndexedDBBackend());
|
||||||
|
SQL.register_for_idb(sqlFS);
|
||||||
|
|
||||||
|
SQL.FS.mkdir("/sql");
|
||||||
|
SQL.FS.mount(sqlFS, {}, "/sql");
|
||||||
|
|
||||||
|
const path = "/sql/timesafari.absurd-sql";
|
||||||
|
if (typeof SharedArrayBuffer === "undefined") {
|
||||||
|
const stream = SQL.FS.open(path, "a+");
|
||||||
|
await stream.node.contents.readIfFallback();
|
||||||
|
SQL.FS.close(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.db = new SQL.Database(path, { filename: true });
|
||||||
|
if (!this.db) {
|
||||||
|
throw new Error(
|
||||||
|
"The database initialization failed. We recommend you restart or reinstall.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// An error is thrown without this pragma: "File has invalid page size. (the first block of a new file must be written first)"
|
||||||
|
await this.db.exec(`PRAGMA journal_mode=MEMORY;`);
|
||||||
|
const sqlExec = this.db.run.bind(this.db);
|
||||||
|
const sqlQuery = this.db.exec.bind(this.db);
|
||||||
|
|
||||||
|
// Extract the migration names for the absurd-sql format
|
||||||
|
const extractMigrationNames: (result: QueryExecResult[]) => Set<string> = (
|
||||||
|
result,
|
||||||
|
) => {
|
||||||
|
// Even with the "select name" query, the QueryExecResult may be [] (which doesn't make sense to me).
|
||||||
|
const names = result?.[0]?.values.map((row) => row[0] as string) || [];
|
||||||
|
return new Set(names);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run migrations
|
||||||
|
await runMigrations(sqlExec, sqlQuery, extractMigrationNames);
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
|
||||||
|
// Start processing the queue after initialization
|
||||||
|
this.processQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async processQueue(): Promise<void> {
|
||||||
|
if (this.isProcessingQueue || !this.initialized || !this.db) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isProcessingQueue = true;
|
||||||
|
|
||||||
|
while (this.operationQueue.length > 0) {
|
||||||
|
const operation = this.operationQueue.shift();
|
||||||
|
if (!operation) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let result: unknown;
|
||||||
|
switch (operation.type) {
|
||||||
|
case "run":
|
||||||
|
result = await this.db.run(operation.sql, operation.params);
|
||||||
|
break;
|
||||||
|
case "query":
|
||||||
|
result = await this.db.exec(operation.sql, operation.params);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
operation.resolve(result);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
"Error while processing SQL queue:",
|
||||||
|
error,
|
||||||
|
" ... for sql:",
|
||||||
|
operation.sql,
|
||||||
|
" ... with params:",
|
||||||
|
operation.params,
|
||||||
|
);
|
||||||
|
operation.reject(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isProcessingQueue = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async queueOperation<R>(
|
||||||
|
type: QueuedOperation["type"],
|
||||||
|
sql: string,
|
||||||
|
params: unknown[] = [],
|
||||||
|
): Promise<R> {
|
||||||
|
return new Promise<R>((resolve, reject) => {
|
||||||
|
const operation: QueuedOperation = {
|
||||||
|
type,
|
||||||
|
sql,
|
||||||
|
params,
|
||||||
|
resolve: (value: unknown) => resolve(value as R),
|
||||||
|
reject,
|
||||||
|
};
|
||||||
|
this.operationQueue.push(operation);
|
||||||
|
|
||||||
|
// If we're already initialized, start processing the queue
|
||||||
|
if (this.initialized && this.db) {
|
||||||
|
this.processQueue();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async waitForInitialization(): Promise<void> {
|
||||||
|
// If we have an initialization promise, wait for it
|
||||||
|
if (this.initializationPromise) {
|
||||||
|
await this.initializationPromise;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not initialized and no promise, start initialization
|
||||||
|
if (!this.initialized) {
|
||||||
|
await this.initialize();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If initialized but no db, something went wrong
|
||||||
|
if (!this.db) {
|
||||||
|
logger.error(
|
||||||
|
`Database not properly initialized after await waitForInitialization() - initialized flag is true but db is null`,
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
`The database could not be initialized. We recommend you restart or reinstall.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used for inserts, updates, and deletes
|
||||||
|
async run(
|
||||||
|
sql: string,
|
||||||
|
params: unknown[] = [],
|
||||||
|
): Promise<{ changes: number; lastId?: number }> {
|
||||||
|
await this.waitForInitialization();
|
||||||
|
return this.queueOperation<{ changes: number; lastId?: number }>(
|
||||||
|
"run",
|
||||||
|
sql,
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note that the resulting array may be empty if there are no results from the query
|
||||||
|
async query(sql: string, params: unknown[] = []): Promise<QueryExecResult[]> {
|
||||||
|
await this.waitForInitialization();
|
||||||
|
return this.queueOperation<QueryExecResult[]>("query", sql, params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a singleton instance
|
||||||
|
const databaseService = AbsurdSqlDatabaseService.getInstance();
|
||||||
|
|
||||||
|
export default databaseService;
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { QueryExecResult } from "@/interfaces/database";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
@@ -106,4 +108,26 @@ export interface PlatformService {
|
|||||||
* @returns Promise that resolves when the deep link has been handled
|
* @returns Promise that resolves when the deep link has been handled
|
||||||
*/
|
*/
|
||||||
handleDeepLink(url: string): Promise<void>;
|
handleDeepLink(url: string): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a SQL query on the database.
|
||||||
|
* @param sql - The SQL query to execute
|
||||||
|
* @param params - The parameters to pass to the query
|
||||||
|
* @returns Promise resolving to the query result
|
||||||
|
*/
|
||||||
|
dbQuery(
|
||||||
|
sql: string,
|
||||||
|
params?: unknown[],
|
||||||
|
): Promise<QueryExecResult | undefined>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a create/update/delete on the database.
|
||||||
|
* @param sql - The SQL statement to execute
|
||||||
|
* @param params - The parameters to pass to the statement
|
||||||
|
* @returns Promise resolving to the result of the statement
|
||||||
|
*/
|
||||||
|
dbExec(
|
||||||
|
sql: string,
|
||||||
|
params?: unknown[],
|
||||||
|
): Promise<{ changes: number; lastId?: number }>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,13 @@ import {
|
|||||||
StartScanOptions,
|
StartScanOptions,
|
||||||
LensFacing,
|
LensFacing,
|
||||||
} from "@capacitor-mlkit/barcode-scanning";
|
} from "@capacitor-mlkit/barcode-scanning";
|
||||||
import { QRScannerService, ScanListener, QRScannerOptions } from "./types";
|
import {
|
||||||
|
QRScannerService,
|
||||||
|
ScanListener,
|
||||||
|
QRScannerOptions,
|
||||||
|
CameraStateListener,
|
||||||
|
CameraState,
|
||||||
|
} from "./types";
|
||||||
import { logger } from "@/utils/logger";
|
import { logger } from "@/utils/logger";
|
||||||
|
|
||||||
export class CapacitorQRScanner implements QRScannerService {
|
export class CapacitorQRScanner implements QRScannerService {
|
||||||
@@ -12,6 +18,9 @@ export class CapacitorQRScanner implements QRScannerService {
|
|||||||
private isScanning = false;
|
private isScanning = false;
|
||||||
private listenerHandles: Array<() => Promise<void>> = [];
|
private listenerHandles: Array<() => Promise<void>> = [];
|
||||||
private cleanupPromise: Promise<void> | null = null;
|
private cleanupPromise: Promise<void> | null = null;
|
||||||
|
private cameraStateListeners: Set<CameraStateListener> = new Set();
|
||||||
|
private currentState: CameraState = "off";
|
||||||
|
private currentStateMessage?: string;
|
||||||
|
|
||||||
async checkPermissions(): Promise<boolean> {
|
async checkPermissions(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
@@ -79,8 +88,11 @@ export class CapacitorQRScanner implements QRScannerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
this.updateCameraState("initializing", "Starting camera...");
|
||||||
|
|
||||||
// Ensure we have permissions before starting
|
// Ensure we have permissions before starting
|
||||||
if (!(await this.checkPermissions())) {
|
if (!(await this.checkPermissions())) {
|
||||||
|
this.updateCameraState("permission_denied", "Camera permission denied");
|
||||||
logger.debug("Requesting camera permissions");
|
logger.debug("Requesting camera permissions");
|
||||||
const granted = await this.requestPermissions();
|
const granted = await this.requestPermissions();
|
||||||
if (!granted) {
|
if (!granted) {
|
||||||
@@ -90,11 +102,16 @@ export class CapacitorQRScanner implements QRScannerService {
|
|||||||
|
|
||||||
// Check if scanning is supported
|
// Check if scanning is supported
|
||||||
if (!(await this.isSupported())) {
|
if (!(await this.isSupported())) {
|
||||||
|
this.updateCameraState(
|
||||||
|
"error",
|
||||||
|
"QR scanning not supported on this device",
|
||||||
|
);
|
||||||
throw new Error("QR scanning not supported on this device");
|
throw new Error("QR scanning not supported on this device");
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("Starting MLKit scanner");
|
logger.info("Starting MLKit scanner");
|
||||||
this.isScanning = true;
|
this.isScanning = true;
|
||||||
|
this.updateCameraState("active", "Camera is active");
|
||||||
|
|
||||||
const scanOptions: StartScanOptions = {
|
const scanOptions: StartScanOptions = {
|
||||||
formats: [BarcodeFormat.QrCode],
|
formats: [BarcodeFormat.QrCode],
|
||||||
@@ -126,6 +143,7 @@ export class CapacitorQRScanner implements QRScannerService {
|
|||||||
stack: wrappedError.stack,
|
stack: wrappedError.stack,
|
||||||
});
|
});
|
||||||
this.isScanning = false;
|
this.isScanning = false;
|
||||||
|
this.updateCameraState("error", wrappedError.message);
|
||||||
await this.cleanup();
|
await this.cleanup();
|
||||||
this.scanListener?.onError?.(wrappedError);
|
this.scanListener?.onError?.(wrappedError);
|
||||||
throw wrappedError;
|
throw wrappedError;
|
||||||
@@ -140,6 +158,7 @@ export class CapacitorQRScanner implements QRScannerService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
logger.debug("Stopping QR scanner");
|
logger.debug("Stopping QR scanner");
|
||||||
|
this.updateCameraState("off", "Camera stopped");
|
||||||
await BarcodeScanner.stopScan();
|
await BarcodeScanner.stopScan();
|
||||||
logger.info("QR scanner stopped successfully");
|
logger.info("QR scanner stopped successfully");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -149,6 +168,7 @@ export class CapacitorQRScanner implements QRScannerService {
|
|||||||
error: wrappedError.message,
|
error: wrappedError.message,
|
||||||
stack: wrappedError.stack,
|
stack: wrappedError.stack,
|
||||||
});
|
});
|
||||||
|
this.updateCameraState("error", wrappedError.message);
|
||||||
this.scanListener?.onError?.(wrappedError);
|
this.scanListener?.onError?.(wrappedError);
|
||||||
throw wrappedError;
|
throw wrappedError;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -207,4 +227,23 @@ export class CapacitorQRScanner implements QRScannerService {
|
|||||||
// No-op for native scanner
|
// No-op for native scanner
|
||||||
callback(null);
|
callback(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addCameraStateListener(listener: CameraStateListener): void {
|
||||||
|
this.cameraStateListeners.add(listener);
|
||||||
|
// Immediately notify the new listener of current state
|
||||||
|
listener.onStateChange(this.currentState, this.currentStateMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeCameraStateListener(listener: CameraStateListener): void {
|
||||||
|
this.cameraStateListeners.delete(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateCameraState(state: CameraState, message?: string): void {
|
||||||
|
this.currentState = state;
|
||||||
|
this.currentStateMessage = message;
|
||||||
|
// Notify all listeners of state change
|
||||||
|
for (const listener of this.cameraStateListeners) {
|
||||||
|
listener.onStateChange(state, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,14 +30,16 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
private cameraStateListeners: Set<CameraStateListener> = new Set();
|
private cameraStateListeners: Set<CameraStateListener> = new Set();
|
||||||
private currentState: CameraState = "off";
|
private currentState: CameraState = "off";
|
||||||
private currentStateMessage?: string;
|
private currentStateMessage?: string;
|
||||||
|
private options: QRScannerOptions;
|
||||||
|
|
||||||
constructor(private options?: QRScannerOptions) {
|
constructor(options?: QRScannerOptions) {
|
||||||
// Generate a short random ID for this scanner instance
|
// Generate a short random ID for this scanner instance
|
||||||
this.id = Math.random().toString(36).substring(2, 8).toUpperCase();
|
this.id = Math.random().toString(36).substring(2, 8).toUpperCase();
|
||||||
|
this.options = options ?? {};
|
||||||
logger.error(
|
logger.error(
|
||||||
`[WebInlineQRScanner:${this.id}] Initializing scanner with options:`,
|
`[WebInlineQRScanner:${this.id}] Initializing scanner with options:`,
|
||||||
{
|
{
|
||||||
...options,
|
...this.options,
|
||||||
buildId: BUILD_ID,
|
buildId: BUILD_ID,
|
||||||
targetFps: this.TARGET_FPS,
|
targetFps: this.TARGET_FPS,
|
||||||
},
|
},
|
||||||
@@ -494,26 +496,34 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async startScan(): Promise<void> {
|
async startScan(options?: QRScannerOptions): Promise<void> {
|
||||||
if (this.isScanning) {
|
if (this.isScanning) {
|
||||||
logger.error(`[WebInlineQRScanner:${this.id}] Scanner already running`);
|
logger.error(`[WebInlineQRScanner:${this.id}] Scanner already running`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update options if provided
|
||||||
|
if (options) {
|
||||||
|
this.options = { ...this.options, ...options };
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.isScanning = true;
|
this.isScanning = true;
|
||||||
this.scanAttempts = 0;
|
this.scanAttempts = 0;
|
||||||
this.lastScanTime = Date.now();
|
this.lastScanTime = Date.now();
|
||||||
this.updateCameraState("initializing", "Starting camera...");
|
this.updateCameraState("initializing", "Starting camera...");
|
||||||
logger.error(`[WebInlineQRScanner:${this.id}] Starting scan`);
|
logger.error(
|
||||||
|
`[WebInlineQRScanner:${this.id}] Starting scan with options:`,
|
||||||
|
this.options,
|
||||||
|
);
|
||||||
|
|
||||||
// Get camera stream
|
// Get camera stream with options
|
||||||
logger.error(
|
logger.error(
|
||||||
`[WebInlineQRScanner:${this.id}] Requesting camera stream...`,
|
`[WebInlineQRScanner:${this.id}] Requesting camera stream...`,
|
||||||
);
|
);
|
||||||
this.stream = await navigator.mediaDevices.getUserMedia({
|
this.stream = await navigator.mediaDevices.getUserMedia({
|
||||||
video: {
|
video: {
|
||||||
facingMode: "environment",
|
facingMode: this.options.camera === "front" ? "user" : "environment",
|
||||||
width: { ideal: 1280 },
|
width: { ideal: 1280 },
|
||||||
height: { ideal: 720 },
|
height: { ideal: 720 },
|
||||||
},
|
},
|
||||||
@@ -527,11 +537,18 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
label: t.label,
|
label: t.label,
|
||||||
readyState: t.readyState,
|
readyState: t.readyState,
|
||||||
})),
|
})),
|
||||||
|
options: this.options,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set up video element
|
// Set up video element
|
||||||
if (this.video) {
|
if (this.video) {
|
||||||
this.video.srcObject = this.stream;
|
this.video.srcObject = this.stream;
|
||||||
|
// Only show preview if showPreview is true
|
||||||
|
if (this.options.showPreview) {
|
||||||
|
this.video.style.display = "block";
|
||||||
|
} else {
|
||||||
|
this.video.style.display = "none";
|
||||||
|
}
|
||||||
await this.video.play();
|
await this.video.play();
|
||||||
logger.error(
|
logger.error(
|
||||||
`[WebInlineQRScanner:${this.id}] Video element started playing`,
|
`[WebInlineQRScanner:${this.id}] Video element started playing`,
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ import {
|
|||||||
routeSchema,
|
routeSchema,
|
||||||
DeepLinkRoute,
|
DeepLinkRoute,
|
||||||
} from "../interfaces/deepLinks";
|
} from "../interfaces/deepLinks";
|
||||||
import { logConsoleAndDb } from "../db";
|
import { logConsoleAndDb } from "../db/databaseUtil";
|
||||||
import type { DeepLinkError } from "../interfaces/deepLinks";
|
import type { DeepLinkError } from "../interfaces/deepLinks";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -119,6 +119,15 @@ export class DeepLinkHandler {
|
|||||||
const [path, queryString] = parts[1].split("?");
|
const [path, queryString] = parts[1].split("?");
|
||||||
const [routePath, param] = path.split("/");
|
const [routePath, param] = path.split("/");
|
||||||
|
|
||||||
|
// Validate route exists before proceeding
|
||||||
|
if (!this.ROUTE_MAP[routePath]) {
|
||||||
|
throw {
|
||||||
|
code: "INVALID_ROUTE",
|
||||||
|
message: `Invalid route path: ${routePath}`,
|
||||||
|
details: { routePath },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const query: Record<string, string> = {};
|
const query: Record<string, string> = {};
|
||||||
if (queryString) {
|
if (queryString) {
|
||||||
new URLSearchParams(queryString).forEach((value, key) => {
|
new URLSearchParams(queryString).forEach((value, key) => {
|
||||||
@@ -128,11 +137,9 @@ export class DeepLinkHandler {
|
|||||||
|
|
||||||
const params: Record<string, string> = {};
|
const params: Record<string, string> = {};
|
||||||
if (param) {
|
if (param) {
|
||||||
if (this.ROUTE_MAP[routePath].paramKey) {
|
// Now we know routePath exists in ROUTE_MAP
|
||||||
params[this.ROUTE_MAP[routePath].paramKey] = param;
|
const routeConfig = this.ROUTE_MAP[routePath];
|
||||||
} else {
|
params[routeConfig.paramKey ?? "id"] = param;
|
||||||
params["id"] = param;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return { path: routePath, params, query };
|
return { path: routePath, params, query };
|
||||||
}
|
}
|
||||||
|
|||||||
60
src/services/migrationService.ts
Normal file
60
src/services/migrationService.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
interface Migration {
|
||||||
|
name: string;
|
||||||
|
sql: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MigrationService {
|
||||||
|
private static instance: MigrationService;
|
||||||
|
private migrations: Migration[] = [];
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
static getInstance(): MigrationService {
|
||||||
|
if (!MigrationService.instance) {
|
||||||
|
MigrationService.instance = new MigrationService();
|
||||||
|
}
|
||||||
|
return MigrationService.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerMigration(migration: Migration) {
|
||||||
|
this.migrations.push(migration);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param sqlExec - A function that executes a SQL statement and returns some update result
|
||||||
|
* @param sqlQuery - A function that executes a SQL query and returns the result in some format
|
||||||
|
* @param extractMigrationNames - A function that extracts the names (string array) from a "select name from migrations" query
|
||||||
|
*/
|
||||||
|
async runMigrations<T>(
|
||||||
|
// note that this does not take parameters because the Capacitor SQLite 'execute' is different
|
||||||
|
sqlExec: (sql: string) => Promise<unknown>,
|
||||||
|
sqlQuery: (sql: string) => Promise<T>,
|
||||||
|
extractMigrationNames: (result: T) => Set<string>,
|
||||||
|
): Promise<void> {
|
||||||
|
// Create migrations table if it doesn't exist
|
||||||
|
await sqlExec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS migrations (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Get list of executed migrations
|
||||||
|
const result1: T = await sqlQuery("SELECT name FROM migrations;");
|
||||||
|
const executedMigrations = extractMigrationNames(result1);
|
||||||
|
|
||||||
|
// Run pending migrations in order
|
||||||
|
for (const migration of this.migrations) {
|
||||||
|
if (!executedMigrations.has(migration.name)) {
|
||||||
|
await sqlExec(migration.sql);
|
||||||
|
|
||||||
|
await sqlExec(
|
||||||
|
`INSERT INTO migrations (name) VALUES ('${migration.name}')`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MigrationService.getInstance();
|
||||||
@@ -1,23 +1,242 @@
|
|||||||
|
import { Filesystem, Directory, Encoding } from "@capacitor/filesystem";
|
||||||
|
import {
|
||||||
|
Camera,
|
||||||
|
CameraResultType,
|
||||||
|
CameraSource,
|
||||||
|
CameraDirection,
|
||||||
|
} from "@capacitor/camera";
|
||||||
|
import { Share } from "@capacitor/share";
|
||||||
|
import {
|
||||||
|
SQLiteConnection,
|
||||||
|
SQLiteDBConnection,
|
||||||
|
CapacitorSQLite,
|
||||||
|
capSQLiteChanges,
|
||||||
|
DBSQLiteValues,
|
||||||
|
} from "@capacitor-community/sqlite";
|
||||||
|
|
||||||
|
import { runMigrations } from "@/db-sql/migration";
|
||||||
|
import { QueryExecResult } from "@/interfaces/database";
|
||||||
import {
|
import {
|
||||||
ImageResult,
|
ImageResult,
|
||||||
PlatformService,
|
PlatformService,
|
||||||
PlatformCapabilities,
|
PlatformCapabilities,
|
||||||
} from "../PlatformService";
|
} from "../PlatformService";
|
||||||
import { Filesystem, Directory, Encoding } from "@capacitor/filesystem";
|
|
||||||
import { Camera, CameraResultType, CameraSource, CameraDirection } from "@capacitor/camera";
|
|
||||||
import { Share } from "@capacitor/share";
|
|
||||||
import { logger } from "../../utils/logger";
|
import { logger } from "../../utils/logger";
|
||||||
|
|
||||||
|
interface QueuedOperation {
|
||||||
|
type: "run" | "query";
|
||||||
|
sql: string;
|
||||||
|
params: unknown[];
|
||||||
|
resolve: (value: unknown) => void;
|
||||||
|
reject: (reason: unknown) => void;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Platform service implementation for Capacitor (mobile) platform.
|
* Platform service implementation for Capacitor (mobile) platform.
|
||||||
* Provides native mobile functionality through Capacitor plugins for:
|
* Provides native mobile functionality through Capacitor plugins for:
|
||||||
* - File system operations
|
* - File system operations
|
||||||
* - Camera and image picker
|
* - Camera and image picker
|
||||||
* - Platform-specific features
|
* - Platform-specific features
|
||||||
|
* - SQLite database operations
|
||||||
*/
|
*/
|
||||||
export class CapacitorPlatformService implements PlatformService {
|
export class CapacitorPlatformService implements PlatformService {
|
||||||
/** Current camera direction */
|
/** Current camera direction */
|
||||||
private currentDirection: CameraDirection = 'BACK';
|
private currentDirection: CameraDirection = "BACK";
|
||||||
|
|
||||||
|
private sqlite: SQLiteConnection;
|
||||||
|
private db: SQLiteDBConnection | null = null;
|
||||||
|
private dbName = "timesafari.sqlite";
|
||||||
|
private initialized = false;
|
||||||
|
private initializationPromise: Promise<void> | null = null;
|
||||||
|
private operationQueue: Array<QueuedOperation> = [];
|
||||||
|
private isProcessingQueue: boolean = false;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.sqlite = new SQLiteConnection(CapacitorSQLite);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async initializeDatabase(): Promise<void> {
|
||||||
|
// If already initialized, return immediately
|
||||||
|
if (this.initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If initialization is in progress, wait for it
|
||||||
|
if (this.initializationPromise) {
|
||||||
|
return this.initializationPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start initialization
|
||||||
|
this.initializationPromise = this._initialize();
|
||||||
|
try {
|
||||||
|
await this.initializationPromise;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
"[CapacitorPlatformService] Initialize method failed:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
this.initializationPromise = null; // Reset on failure
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _initialize(): Promise<void> {
|
||||||
|
if (this.initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create/Open database
|
||||||
|
this.db = await this.sqlite.createConnection(
|
||||||
|
this.dbName,
|
||||||
|
false,
|
||||||
|
"no-encryption",
|
||||||
|
1,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.db.open();
|
||||||
|
|
||||||
|
// Set journal mode to WAL for better performance
|
||||||
|
// await this.db.execute("PRAGMA journal_mode=WAL;");
|
||||||
|
|
||||||
|
// Run migrations
|
||||||
|
await this.runCapacitorMigrations();
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
logger.log(
|
||||||
|
"[CapacitorPlatformService] SQLite database initialized successfully",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Start processing the queue after initialization
|
||||||
|
this.processQueue();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
"[CapacitorPlatformService] Error initializing SQLite database:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
"[CapacitorPlatformService] Failed to initialize database",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async processQueue(): Promise<void> {
|
||||||
|
if (this.isProcessingQueue || !this.initialized || !this.db) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isProcessingQueue = true;
|
||||||
|
|
||||||
|
while (this.operationQueue.length > 0) {
|
||||||
|
const operation = this.operationQueue.shift();
|
||||||
|
if (!operation) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let result: unknown;
|
||||||
|
switch (operation.type) {
|
||||||
|
case "run": {
|
||||||
|
const runResult = await this.db.run(
|
||||||
|
operation.sql,
|
||||||
|
operation.params,
|
||||||
|
);
|
||||||
|
result = {
|
||||||
|
changes: runResult.changes?.changes || 0,
|
||||||
|
lastId: runResult.changes?.lastId,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "query": {
|
||||||
|
const queryResult = await this.db.query(
|
||||||
|
operation.sql,
|
||||||
|
operation.params,
|
||||||
|
);
|
||||||
|
result = {
|
||||||
|
columns: Object.keys(queryResult.values?.[0] || {}),
|
||||||
|
values: (queryResult.values || []).map((row) =>
|
||||||
|
Object.values(row),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
operation.resolve(result);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
"[CapacitorPlatformService] Error while processing SQL queue:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
operation.reject(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isProcessingQueue = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async queueOperation<R>(
|
||||||
|
type: QueuedOperation["type"],
|
||||||
|
sql: string,
|
||||||
|
params: unknown[] = [],
|
||||||
|
): Promise<R> {
|
||||||
|
return new Promise<R>((resolve, reject) => {
|
||||||
|
const operation: QueuedOperation = {
|
||||||
|
type,
|
||||||
|
sql,
|
||||||
|
params,
|
||||||
|
resolve: (value: unknown) => resolve(value as R),
|
||||||
|
reject,
|
||||||
|
};
|
||||||
|
this.operationQueue.push(operation);
|
||||||
|
|
||||||
|
// If we're already initialized, start processing the queue
|
||||||
|
if (this.initialized && this.db) {
|
||||||
|
this.processQueue();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async waitForInitialization(): Promise<void> {
|
||||||
|
// If we have an initialization promise, wait for it
|
||||||
|
if (this.initializationPromise) {
|
||||||
|
await this.initializationPromise;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not initialized and no promise, start initialization
|
||||||
|
if (!this.initialized) {
|
||||||
|
await this.initializeDatabase();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If initialized but no db, something went wrong
|
||||||
|
if (!this.db) {
|
||||||
|
logger.error(
|
||||||
|
"[CapacitorPlatformService] Database not properly initialized after await waitForInitialization() - initialized flag is true but db is null",
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
"[CapacitorPlatformService] The database could not be initialized. We recommend you restart or reinstall.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async runCapacitorMigrations(): Promise<void> {
|
||||||
|
if (!this.db) {
|
||||||
|
throw new Error("Database not initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
const sqlExec: (sql: string) => Promise<capSQLiteChanges> =
|
||||||
|
this.db.execute.bind(this.db);
|
||||||
|
const sqlQuery: (sql: string) => Promise<DBSQLiteValues> =
|
||||||
|
this.db.query.bind(this.db);
|
||||||
|
const extractMigrationNames: (result: DBSQLiteValues) => Set<string> = (
|
||||||
|
result,
|
||||||
|
) => {
|
||||||
|
const names =
|
||||||
|
result.values?.map((row: { name: string }) => row.name) || [];
|
||||||
|
return new Set(names);
|
||||||
|
};
|
||||||
|
runMigrations(sqlExec, sqlQuery, extractMigrationNames);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the capabilities of the Capacitor platform
|
* Gets the capabilities of the Capacitor platform
|
||||||
@@ -189,6 +408,9 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
*/
|
*/
|
||||||
async writeFile(fileName: string, content: string): Promise<void> {
|
async writeFile(fileName: string, content: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
// Check storage permissions before proceeding
|
||||||
|
await this.checkStoragePermissions();
|
||||||
|
|
||||||
const logData = {
|
const logData = {
|
||||||
targetFileName: fileName,
|
targetFileName: fileName,
|
||||||
contentLength: content.length,
|
contentLength: content.length,
|
||||||
@@ -330,6 +552,9 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
logger.log("[CapacitorPlatformService]", JSON.stringify(logData, null, 2));
|
logger.log("[CapacitorPlatformService]", JSON.stringify(logData, null, 2));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Check storage permissions before proceeding
|
||||||
|
await this.checkStoragePermissions();
|
||||||
|
|
||||||
const { uri } = await Filesystem.writeFile({
|
const { uri } = await Filesystem.writeFile({
|
||||||
path: fileName,
|
path: fileName,
|
||||||
data: content,
|
data: content,
|
||||||
@@ -476,7 +701,7 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
* @returns Promise that resolves when the camera is rotated
|
* @returns Promise that resolves when the camera is rotated
|
||||||
*/
|
*/
|
||||||
async rotateCamera(): Promise<void> {
|
async rotateCamera(): Promise<void> {
|
||||||
this.currentDirection = this.currentDirection === 'BACK' ? 'FRONT' : 'BACK';
|
this.currentDirection = this.currentDirection === "BACK" ? "FRONT" : "BACK";
|
||||||
logger.debug(`Camera rotated to ${this.currentDirection} camera`);
|
logger.debug(`Camera rotated to ${this.currentDirection} camera`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -490,4 +715,27 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
// This is just a placeholder for the interface
|
// This is just a placeholder for the interface
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see PlatformService.dbQuery
|
||||||
|
*/
|
||||||
|
async dbQuery(sql: string, params?: unknown[]): Promise<QueryExecResult> {
|
||||||
|
await this.waitForInitialization();
|
||||||
|
return this.queueOperation<QueryExecResult>("query", sql, params || []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see PlatformService.dbExec
|
||||||
|
*/
|
||||||
|
async dbExec(
|
||||||
|
sql: string,
|
||||||
|
params?: unknown[],
|
||||||
|
): Promise<{ changes: number; lastId?: number }> {
|
||||||
|
await this.waitForInitialization();
|
||||||
|
return this.queueOperation<{ changes: number; lastId?: number }>(
|
||||||
|
"run",
|
||||||
|
sql,
|
||||||
|
params || [],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,20 +4,195 @@ import {
|
|||||||
PlatformCapabilities,
|
PlatformCapabilities,
|
||||||
} 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";
|
||||||
|
|
||||||
|
interface Migration {
|
||||||
|
name: string;
|
||||||
|
sql: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Platform service implementation for Electron (desktop) platform.
|
* Platform service implementation for Electron (desktop) platform.
|
||||||
* Note: This is a placeholder implementation with most methods currently unimplemented.
|
* Provides native desktop functionality through Electron and Capacitor plugins for:
|
||||||
* Implements the PlatformService interface but throws "Not implemented" errors for most operations.
|
* - File system operations (TODO)
|
||||||
*
|
* - Camera integration (TODO)
|
||||||
* @remarks
|
* - SQLite database operations
|
||||||
* This service is intended for desktop application functionality through Electron.
|
* - System-level features (TODO)
|
||||||
* Future implementations should provide:
|
|
||||||
* - Native file system access
|
|
||||||
* - Desktop camera integration
|
|
||||||
* - System-level features
|
|
||||||
*/
|
*/
|
||||||
export class ElectronPlatformService implements PlatformService {
|
export class ElectronPlatformService implements PlatformService {
|
||||||
|
private sqlite: SQLiteConnection;
|
||||||
|
private db: SQLiteDBConnection | null = null;
|
||||||
|
private dbName = "timesafari.db";
|
||||||
|
private initialized = false;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.sqlite = new SQLiteConnection(CapacitorSQLite);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async initializeDatabase(): Promise<void> {
|
||||||
|
if (this.initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create/Open database
|
||||||
|
this.db = await this.sqlite.createConnection(
|
||||||
|
this.dbName,
|
||||||
|
false,
|
||||||
|
"no-encryption",
|
||||||
|
1,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.db.open();
|
||||||
|
|
||||||
|
// Set journal mode to WAL for better performance
|
||||||
|
await this.db.execute("PRAGMA journal_mode=WAL;");
|
||||||
|
|
||||||
|
// Run migrations
|
||||||
|
await this.runMigrations();
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
logger.log(
|
||||||
|
"[ElectronPlatformService] SQLite database initialized successfully",
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
"[ElectronPlatformService] Error initializing SQLite database:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
throw new Error(
|
||||||
|
"[ElectronPlatformService] Failed to initialize database",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async runMigrations(): Promise<void> {
|
||||||
|
if (!this.db) {
|
||||||
|
throw new Error("Database not initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create migrations table if it doesn't exist
|
||||||
|
await this.db.execute(`
|
||||||
|
CREATE TABLE IF NOT EXISTS migrations (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Get list of executed migrations
|
||||||
|
const result = await this.db.query("SELECT name FROM migrations;");
|
||||||
|
const executedMigrations = new Set(
|
||||||
|
result.values?.map((row) => row[0]) || [],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Run pending migrations in order
|
||||||
|
const migrations: Migration[] = [
|
||||||
|
{
|
||||||
|
name: "001_initial",
|
||||||
|
sql: `
|
||||||
|
CREATE TABLE IF NOT EXISTS accounts (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
dateCreated TEXT NOT NULL,
|
||||||
|
derivationPath TEXT,
|
||||||
|
did TEXT NOT NULL,
|
||||||
|
identityEncrBase64 TEXT,
|
||||||
|
mnemonicEncrBase64 TEXT,
|
||||||
|
passkeyCredIdHex TEXT,
|
||||||
|
publicKeyHex TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_accounts_did ON accounts(did);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS secret (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
secretBase64 TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
accountDid TEXT,
|
||||||
|
activeDid TEXT,
|
||||||
|
apiServer TEXT,
|
||||||
|
filterFeedByNearby BOOLEAN,
|
||||||
|
filterFeedByVisible BOOLEAN,
|
||||||
|
finishedOnboarding BOOLEAN,
|
||||||
|
firstName TEXT,
|
||||||
|
hideRegisterPromptOnNewContact BOOLEAN,
|
||||||
|
isRegistered BOOLEAN,
|
||||||
|
lastName TEXT,
|
||||||
|
lastAckedOfferToUserJwtId TEXT,
|
||||||
|
lastAckedOfferToUserProjectsJwtId TEXT,
|
||||||
|
lastNotifiedClaimId TEXT,
|
||||||
|
lastViewedClaimId TEXT,
|
||||||
|
notifyingNewActivityTime TEXT,
|
||||||
|
notifyingReminderMessage TEXT,
|
||||||
|
notifyingReminderTime TEXT,
|
||||||
|
partnerApiServer TEXT,
|
||||||
|
passkeyExpirationMinutes INTEGER,
|
||||||
|
profileImageUrl TEXT,
|
||||||
|
searchBoxes TEXT,
|
||||||
|
showContactGivesInline BOOLEAN,
|
||||||
|
showGeneralAdvanced BOOLEAN,
|
||||||
|
showShortcutBvc BOOLEAN,
|
||||||
|
vapid TEXT,
|
||||||
|
warnIfProdServer BOOLEAN,
|
||||||
|
warnIfTestServer BOOLEAN,
|
||||||
|
webPushServer TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_settings_accountDid ON settings(accountDid);
|
||||||
|
|
||||||
|
INSERT INTO settings (id, apiServer) VALUES (1, '${DEFAULT_ENDORSER_API_SERVER}');
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS contacts (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
did TEXT NOT NULL,
|
||||||
|
name TEXT,
|
||||||
|
contactMethods TEXT,
|
||||||
|
nextPubKeyHashB64 TEXT,
|
||||||
|
notes TEXT,
|
||||||
|
profileImageUrl TEXT,
|
||||||
|
publicKeyBase64 TEXT,
|
||||||
|
seesMe BOOLEAN,
|
||||||
|
registered BOOLEAN
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_contacts_did ON contacts(did);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_contacts_name ON contacts(name);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS logs (
|
||||||
|
date TEXT PRIMARY KEY,
|
||||||
|
message TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS temp (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
blobB64 TEXT
|
||||||
|
);
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const migration of migrations) {
|
||||||
|
if (!executedMigrations.has(migration.name)) {
|
||||||
|
await this.db.execute(migration.sql);
|
||||||
|
await this.db.run("INSERT INTO migrations (name) VALUES (?)", [
|
||||||
|
migration.name,
|
||||||
|
]);
|
||||||
|
logger.log(`Migration ${migration.name} executed successfully`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the capabilities of the Electron platform
|
* Gets the capabilities of the Electron platform
|
||||||
* @returns Platform capabilities object
|
* @returns Platform capabilities object
|
||||||
@@ -55,6 +230,17 @@ export class ElectronPlatformService implements PlatformService {
|
|||||||
throw new Error("Not implemented");
|
throw new Error("Not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes content to a file and opens the system share dialog.
|
||||||
|
* @param _fileName - Name of the file to create
|
||||||
|
* @param _content - Content to write to the file
|
||||||
|
* @throws Error with "Not implemented" message
|
||||||
|
* @todo Implement using Electron's dialog and file system APIs
|
||||||
|
*/
|
||||||
|
async writeAndShareFile(_fileName: string, _content: string): Promise<void> {
|
||||||
|
throw new Error("Not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a file from the filesystem.
|
* Deletes a file from the filesystem.
|
||||||
* @param _path - Path to the file to delete
|
* @param _path - Path to the file to delete
|
||||||
@@ -108,4 +294,55 @@ export class ElectronPlatformService implements PlatformService {
|
|||||||
logger.error("handleDeepLink not implemented in Electron platform");
|
logger.error("handleDeepLink not implemented in Electron platform");
|
||||||
throw new Error("Not implemented");
|
throw new Error("Not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see PlatformService.dbQuery
|
||||||
|
*/
|
||||||
|
async dbQuery(sql: string, params?: unknown[]): Promise<QueryExecResult> {
|
||||||
|
await this.initializeDatabase();
|
||||||
|
if (!this.db) {
|
||||||
|
throw new Error("Database not initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.db.query(sql, params || []);
|
||||||
|
const values = result.values || [];
|
||||||
|
return {
|
||||||
|
columns: [], // SQLite plugin doesn't provide column names in query result
|
||||||
|
values: values as SqlValue[][],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error executing query:", error);
|
||||||
|
throw new Error(
|
||||||
|
`Database query failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see PlatformService.dbExec
|
||||||
|
*/
|
||||||
|
async dbExec(
|
||||||
|
sql: string,
|
||||||
|
params?: unknown[],
|
||||||
|
): Promise<{ changes: number; lastId?: number }> {
|
||||||
|
await this.initializeDatabase();
|
||||||
|
if (!this.db) {
|
||||||
|
throw new Error("Database not initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.db.run(sql, params || []);
|
||||||
|
const changes = result.changes as Changes;
|
||||||
|
return {
|
||||||
|
changes: changes?.changes || 0,
|
||||||
|
lastId: changes?.lastId,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error executing statement:", error);
|
||||||
|
throw new Error(
|
||||||
|
`Database execution failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
PlatformCapabilities,
|
PlatformCapabilities,
|
||||||
} from "../PlatformService";
|
} from "../PlatformService";
|
||||||
import { logger } from "../../utils/logger";
|
import { logger } from "../../utils/logger";
|
||||||
|
import { QueryExecResult } from "@/interfaces/database";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Platform service implementation for PyWebView platform.
|
* Platform service implementation for PyWebView platform.
|
||||||
@@ -109,4 +110,26 @@ export class PyWebViewPlatformService implements PlatformService {
|
|||||||
logger.error("handleDeepLink not implemented in PyWebView platform");
|
logger.error("handleDeepLink not implemented in PyWebView platform");
|
||||||
throw new Error("Not implemented");
|
throw new Error("Not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dbQuery(sql: string, params?: unknown[]): Promise<QueryExecResult> {
|
||||||
|
throw new Error("Not implemented for " + sql + " with params " + params);
|
||||||
|
}
|
||||||
|
dbExec(
|
||||||
|
sql: string,
|
||||||
|
params?: unknown[],
|
||||||
|
): Promise<{ changes: number; lastId?: number }> {
|
||||||
|
throw new Error("Not implemented for " + sql + " with params " + params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should write and share a file using the Python backend.
|
||||||
|
* @param _fileName - Name of the file to write and share
|
||||||
|
* @param _content - Content to write to the file
|
||||||
|
* @throws Error with "Not implemented" message
|
||||||
|
* @todo Implement file writing and sharing through pywebview's Python-JavaScript bridge
|
||||||
|
*/
|
||||||
|
async writeAndShareFile(_fileName: string, _content: string): Promise<void> {
|
||||||
|
logger.error("writeAndShareFile not implemented in PyWebView platform");
|
||||||
|
throw new Error("Not implemented");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import {
|
|||||||
PlatformCapabilities,
|
PlatformCapabilities,
|
||||||
} from "../PlatformService";
|
} from "../PlatformService";
|
||||||
import { logger } from "../../utils/logger";
|
import { logger } from "../../utils/logger";
|
||||||
|
import { QueryExecResult } from "@/interfaces/database";
|
||||||
|
import databaseService from "../AbsurdSqlDatabaseService";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Platform service implementation for web browser platform.
|
* Platform service implementation for web browser platform.
|
||||||
@@ -359,4 +361,33 @@ export class WebPlatformService implements PlatformService {
|
|||||||
async writeAndShareFile(_fileName: string, _content: string): Promise<void> {
|
async writeAndShareFile(_fileName: string, _content: string): Promise<void> {
|
||||||
throw new Error("File system access not available in web platform");
|
throw new Error("File system access not available in web platform");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see PlatformService.dbQuery
|
||||||
|
*/
|
||||||
|
dbQuery(
|
||||||
|
sql: string,
|
||||||
|
params?: unknown[],
|
||||||
|
): Promise<QueryExecResult | undefined> {
|
||||||
|
return databaseService.query(sql, params).then((result) => result[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see PlatformService.dbExec
|
||||||
|
*/
|
||||||
|
dbExec(
|
||||||
|
sql: string,
|
||||||
|
params?: unknown[],
|
||||||
|
): Promise<{ changes: number; lastId?: number }> {
|
||||||
|
return databaseService.run(sql, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
async dbGetOneRow(
|
||||||
|
sql: string,
|
||||||
|
params?: unknown[],
|
||||||
|
): Promise<unknown[] | undefined> {
|
||||||
|
return databaseService
|
||||||
|
.query(sql, params)
|
||||||
|
.then((result: QueryExecResult[]) => result[0]?.values[0]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import * as didJwt from "did-jwt";
|
import * as didJwt from "did-jwt";
|
||||||
import { AppString } from "../constants/app";
|
import { AppString, USE_DEXIE_DB } from "../constants/app";
|
||||||
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
import { retrieveSettingsForActiveAccount } from "../db";
|
import { retrieveSettingsForActiveAccount } from "../db";
|
||||||
import { SERVICE_ID } from "../libs/endorserServer";
|
import { SERVICE_ID } from "../libs/endorserServer";
|
||||||
import { deriveAddress, newIdentifier } from "../libs/crypto";
|
import { deriveAddress, newIdentifier } from "../libs/crypto";
|
||||||
@@ -16,7 +17,10 @@ export async function testServerRegisterUser() {
|
|||||||
|
|
||||||
const identity0 = newIdentifier(addr, publicHex, privateHex, deriPath);
|
const identity0 = newIdentifier(addr, publicHex, privateHex, deriPath);
|
||||||
|
|
||||||
const settings = await retrieveSettingsForActiveAccount();
|
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
settings = await retrieveSettingsForActiveAccount();
|
||||||
|
}
|
||||||
|
|
||||||
// Make a claim
|
// Make a claim
|
||||||
const vcClaim = {
|
const vcClaim = {
|
||||||
|
|||||||
45
src/types/absurd-sql.d.ts
vendored
Normal file
45
src/types/absurd-sql.d.ts
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
declare module 'absurd-sql/dist/indexeddb-backend' {
|
||||||
|
export default class IndexedDBBackend {
|
||||||
|
constructor(options?: {
|
||||||
|
dbName?: string;
|
||||||
|
storeName?: string;
|
||||||
|
onReady?: () => void;
|
||||||
|
onError?: (error: Error) => void;
|
||||||
|
});
|
||||||
|
init(): Promise<void>;
|
||||||
|
exec(sql: string, params?: any[]): Promise<any>;
|
||||||
|
close(): Promise<void>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'absurd-sql/dist/indexeddb-main-thread' {
|
||||||
|
export function initBackend(worker: Worker): Promise<void>;
|
||||||
|
|
||||||
|
export default class IndexedDBMainThread {
|
||||||
|
constructor(options?: {
|
||||||
|
dbName?: string;
|
||||||
|
storeName?: string;
|
||||||
|
onReady?: () => void;
|
||||||
|
onError?: (error: Error) => void;
|
||||||
|
});
|
||||||
|
init(): Promise<void>;
|
||||||
|
exec(sql: string, params?: any[]): Promise<any>;
|
||||||
|
close(): Promise<void>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'absurd-sql' {
|
||||||
|
export class SQLiteFS {
|
||||||
|
constructor(fs: unknown, backend: IndexedDBBackend);
|
||||||
|
init(): Promise<void>;
|
||||||
|
close(): Promise<void>;
|
||||||
|
exec(sql: string, params?: any[]): Promise<any>;
|
||||||
|
prepare(sql: string): Promise<any>;
|
||||||
|
run(sql: string, params?: any[]): Promise<any>;
|
||||||
|
get(sql: string, params?: any[]): Promise<any>;
|
||||||
|
all(sql: string, params?: any[]): Promise<any[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export * from 'absurd-sql/dist/indexeddb-backend';
|
||||||
|
export * from 'absurd-sql/dist/indexeddb-main-thread';
|
||||||
|
}
|
||||||
36
src/types/global.d.ts
vendored
Normal file
36
src/types/global.d.ts
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import type { QueryExecResult, SqlValue } from "./database";
|
||||||
|
|
||||||
|
declare module '@jlongster/sql.js' {
|
||||||
|
interface SQL {
|
||||||
|
Database: new (path: string, options?: { filename: boolean }) => Database;
|
||||||
|
FS: {
|
||||||
|
mkdir: (path: string) => void;
|
||||||
|
mount: (fs: any, options: any, path: string) => void;
|
||||||
|
open: (path: string, flags: string) => any;
|
||||||
|
close: (stream: any) => void;
|
||||||
|
};
|
||||||
|
register_for_idb: (fs: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Database {
|
||||||
|
exec: (sql: string, params?: unknown[]) => Promise<QueryExecResult[]>;
|
||||||
|
run: (sql: string, params?: unknown[]) => Promise<{ changes: number; lastId?: number }>;
|
||||||
|
get: (sql: string, params?: unknown[]) => Promise<SqlValue[]>;
|
||||||
|
all: (sql: string, params?: unknown[]) => Promise<SqlValue[][]>;
|
||||||
|
prepare: (sql: string) => Promise<Statement>;
|
||||||
|
close: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Statement {
|
||||||
|
run: (params?: unknown[]) => Promise<{ changes: number; lastId?: number }>;
|
||||||
|
get: (params?: unknown[]) => Promise<SqlValue[]>;
|
||||||
|
all: (params?: unknown[]) => Promise<SqlValue[][]>;
|
||||||
|
finalize: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initSqlJs: (options?: {
|
||||||
|
locateFile?: (file: string) => string;
|
||||||
|
}) => Promise<SQL>;
|
||||||
|
|
||||||
|
export default initSqlJs;
|
||||||
|
}
|
||||||
67
src/types/modules.d.ts
vendored
Normal file
67
src/types/modules.d.ts
vendored
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import type { QueryExecResult, SqlValue } from "./database";
|
||||||
|
|
||||||
|
declare module '@jlongster/sql.js' {
|
||||||
|
interface SQL {
|
||||||
|
Database: new (path: string, options?: { filename: boolean }) => Database;
|
||||||
|
FS: {
|
||||||
|
mkdir: (path: string) => void;
|
||||||
|
mount: (fs: any, options: any, path: string) => void;
|
||||||
|
open: (path: string, flags: string) => any;
|
||||||
|
close: (stream: any) => void;
|
||||||
|
};
|
||||||
|
register_for_idb: (fs: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Database {
|
||||||
|
exec: (sql: string, params?: unknown[]) => Promise<QueryExecResult[]>;
|
||||||
|
run: (sql: string, params?: unknown[]) => Promise<{ changes: number; lastId?: number }>;
|
||||||
|
get: (sql: string, params?: unknown[]) => Promise<SqlValue[]>;
|
||||||
|
all: (sql: string, params?: unknown[]) => Promise<SqlValue[][]>;
|
||||||
|
prepare: (sql: string) => Promise<Statement>;
|
||||||
|
close: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Statement {
|
||||||
|
run: (params?: unknown[]) => Promise<{ changes: number; lastId?: number }>;
|
||||||
|
get: (params?: unknown[]) => Promise<SqlValue[]>;
|
||||||
|
all: (params?: unknown[]) => Promise<SqlValue[][]>;
|
||||||
|
finalize: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initSqlJs: (options?: {
|
||||||
|
locateFile?: (file: string) => string;
|
||||||
|
}) => Promise<SQL>;
|
||||||
|
|
||||||
|
export default initSqlJs;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'absurd-sql' {
|
||||||
|
import type { SQL } from '@jlongster/sql.js';
|
||||||
|
export class SQLiteFS {
|
||||||
|
constructor(fs: any, backend: any);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'absurd-sql/dist/indexeddb-backend' {
|
||||||
|
export default class IndexedDBBackend {
|
||||||
|
constructor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'absurd-sql/dist/indexeddb-main-thread' {
|
||||||
|
import type { QueryExecResult } from './database';
|
||||||
|
export interface SQLiteOptions {
|
||||||
|
filename?: string;
|
||||||
|
autoLoad?: boolean;
|
||||||
|
debug?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SQLiteDatabase {
|
||||||
|
exec: (sql: string, params?: unknown[]) => Promise<QueryExecResult[]>;
|
||||||
|
close: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initSqlJs(options?: any): Promise<any>;
|
||||||
|
export function createDatabase(options?: SQLiteOptions): Promise<SQLiteDatabase>;
|
||||||
|
export function openDatabase(options?: SQLiteOptions): Promise<SQLiteDatabase>;
|
||||||
|
}
|
||||||
57
src/types/sql.js.d.ts
vendored
Normal file
57
src/types/sql.js.d.ts
vendored
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* Type definitions for @jlongster/sql.js
|
||||||
|
* @author Matthew Raymer
|
||||||
|
* @description TypeScript declaration file for the SQL.js WASM module with filesystem support
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare module '@jlongster/sql.js' {
|
||||||
|
export interface FileSystem {
|
||||||
|
mkdir(path: string): void;
|
||||||
|
mount(fs: any, opts: any, mountpoint: string): void;
|
||||||
|
open(path: string, flags: string): FileStream;
|
||||||
|
close(stream: FileStream): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileStream {
|
||||||
|
node: {
|
||||||
|
contents: {
|
||||||
|
readIfFallback(): Promise<void>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Database {
|
||||||
|
exec(sql: string, params?: any[]): Promise<QueryExecResult[]>;
|
||||||
|
prepare(sql: string): Statement;
|
||||||
|
run(sql: string, params?: any[]): Promise<{ changes: number; lastId?: number }>;
|
||||||
|
close(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryExecResult {
|
||||||
|
columns: string[];
|
||||||
|
values: any[][];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Statement {
|
||||||
|
bind(params: any[]): void;
|
||||||
|
step(): boolean;
|
||||||
|
get(): any[];
|
||||||
|
getColumnNames(): string[];
|
||||||
|
reset(): void;
|
||||||
|
free(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InitSqlJsStatic {
|
||||||
|
(config?: {
|
||||||
|
locateFile?: (file: string) => string;
|
||||||
|
wasmBinary?: ArrayBuffer;
|
||||||
|
}): Promise<{
|
||||||
|
Database: new (path?: string | Uint8Array, opts?: { filename?: boolean }) => Database;
|
||||||
|
FS: FileSystem;
|
||||||
|
register_for_idb: (fs: any) => void;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initSqlJs: InitSqlJsStatic;
|
||||||
|
export default initSqlJs;
|
||||||
|
}
|
||||||
2
src/utils/empty-module.js
Normal file
2
src/utils/empty-module.js
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// Empty module to satisfy Node.js built-in module imports
|
||||||
|
export default {};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { logToDb } from "../db";
|
import { logToDb } from "../db/databaseUtil";
|
||||||
|
|
||||||
function safeStringify(obj: unknown) {
|
function safeStringify(obj: unknown) {
|
||||||
const seen = new WeakSet();
|
const seen = new WeakSet();
|
||||||
@@ -24,8 +24,8 @@ export const logger = {
|
|||||||
if (process.env.NODE_ENV !== "production") {
|
if (process.env.NODE_ENV !== "production") {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.debug(message, ...args);
|
console.debug(message, ...args);
|
||||||
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
|
// const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
|
||||||
logToDb(message + argsString);
|
// logToDb(message + argsString);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
log: (message: string, ...args: unknown[]) => {
|
log: (message: string, ...args: unknown[]) => {
|
||||||
@@ -42,7 +42,8 @@ export const logger = {
|
|||||||
info: (message: string, ...args: unknown[]) => {
|
info: (message: string, ...args: unknown[]) => {
|
||||||
if (
|
if (
|
||||||
process.env.NODE_ENV !== "production" ||
|
process.env.NODE_ENV !== "production" ||
|
||||||
process.env.VITE_PLATFORM === "capacitor"
|
process.env.VITE_PLATFORM === "capacitor" ||
|
||||||
|
process.env.VITE_PLATFORM === "electron"
|
||||||
) {
|
) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.info(message, ...args);
|
console.info(message, ...args);
|
||||||
@@ -53,7 +54,8 @@ export const logger = {
|
|||||||
warn: (message: string, ...args: unknown[]) => {
|
warn: (message: string, ...args: unknown[]) => {
|
||||||
if (
|
if (
|
||||||
process.env.NODE_ENV !== "production" ||
|
process.env.NODE_ENV !== "production" ||
|
||||||
process.env.VITE_PLATFORM === "capacitor"
|
process.env.VITE_PLATFORM === "capacitor" ||
|
||||||
|
process.env.VITE_PLATFORM === "electron"
|
||||||
) {
|
) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.warn(message, ...args);
|
console.warn(message, ...args);
|
||||||
|
|||||||
17
src/utils/node-modules/crypto.js
Normal file
17
src/utils/node-modules/crypto.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// Minimal crypto module implementation for browser using Web Crypto API
|
||||||
|
const crypto = {
|
||||||
|
...window.crypto,
|
||||||
|
// Add any Node.js crypto methods that might be needed
|
||||||
|
randomBytes: (size) => {
|
||||||
|
const buffer = new Uint8Array(size);
|
||||||
|
window.crypto.getRandomValues(buffer);
|
||||||
|
return buffer;
|
||||||
|
},
|
||||||
|
createHash: () => ({
|
||||||
|
update: () => ({
|
||||||
|
digest: () => new Uint8Array(32), // Return empty hash
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default crypto;
|
||||||
18
src/utils/node-modules/fs.js
Normal file
18
src/utils/node-modules/fs.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// Minimal fs module implementation for browser
|
||||||
|
const fs = {
|
||||||
|
readFileSync: () => {
|
||||||
|
throw new Error("fs.readFileSync is not supported in browser");
|
||||||
|
},
|
||||||
|
writeFileSync: () => {
|
||||||
|
throw new Error("fs.writeFileSync is not supported in browser");
|
||||||
|
},
|
||||||
|
existsSync: () => false,
|
||||||
|
mkdirSync: () => {},
|
||||||
|
readdirSync: () => [],
|
||||||
|
statSync: () => ({
|
||||||
|
isDirectory: () => false,
|
||||||
|
isFile: () => false,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default fs;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user