Compare commits
229 Commits
sql-absurd
...
gifting-pe
| Author | SHA1 | Date | |
|---|---|---|---|
| 25512d3db1 | |||
|
|
bf9fee7ee9 | ||
|
|
08c46a27d3 | ||
|
|
c9405839c3 | ||
| 0e6a9c4f89 | |||
|
|
b6278ca148 | ||
|
|
d8e237f8cb | ||
|
|
4b539ccc55 | ||
|
|
ea49173885 | ||
|
|
447a7cb089 | ||
|
|
c0ddba8898 | ||
|
|
fe4ae90849 | ||
|
|
ce04312baa | ||
|
|
a8cc480960 | ||
|
|
357822d713 | ||
| 3baa6633a6 | |||
| bda98eb632 | |||
| eea1cb995a | |||
| 276e0a741b | |||
| e46d6133fb | |||
| 94994a7251 | |||
| 838723c26b | |||
| bb6eb92ba1 | |||
| a997d4cb92 | |||
| 73733345ff | |||
| 5aa693de63 | |||
| 6f2272eea7 | |||
| 9b69c0b22c | |||
| ab2270d8b2 | |||
|
|
ca22161f12 | ||
| 0cf5cf266d | |||
|
|
d3b80fbe47 | ||
|
|
0342c872f4 | ||
|
|
4d01f64fe7 | ||
|
|
d1f61e3530 | ||
| 4162208b7f | |||
| 731605e244 | |||
| 5dd2bf4c6e | |||
| 75a4a1d901 | |||
| 8e605d04d7 | |||
| da0b244bae | |||
| 6136cafd11 | |||
| a248e9a5a3 | |||
| 452ae555bb | |||
| 3df5e19d9d | |||
| 8eff407a9c | |||
| e759e4785b | |||
|
|
9d054074e4 | ||
|
|
30de30e709 | ||
|
|
a7e65b3b49 | ||
|
|
6cbd32af94 | ||
|
|
30c8b73041 | ||
|
|
2f9ab14c88 | ||
|
|
8a7f142cb7 | ||
|
|
f375a4e11a | ||
| 3118f71320 | |||
| d12f23aa81 | |||
| e9a8a3c1e7 | |||
| 1e0efe6011 | |||
| 16557f1e4b | |||
| c4a54967bc | |||
| 20ade415dc | |||
| 6689520270 | |||
| 3fd6c2b80d | |||
|
|
eb7605991c | ||
|
|
40a2491d68 | ||
|
|
25c1d6ef4e | ||
| a5c5c2b9dd | |||
| cf33a39fbc | |||
| 8629cefa13 | |||
| 5e851e442f | |||
| 4a43bc9c6c | |||
| 60de8cee62 | |||
|
|
bb2a4ab76e | ||
|
|
048dded278 | ||
| e240c2940a | |||
| fa21660fd1 | |||
| 54dca9e745 | |||
| 9f0fed0a60 | |||
| 0d152adbf2 | |||
| cead308800 | |||
| 676a301331 | |||
| d6db81cc36 | |||
|
|
f2ddcd2541 | ||
| fb81f7b96e | |||
|
|
df1c1f0186 | ||
| a23416ead1 | |||
| 530c7c1a13 | |||
|
|
3daf1c8a5c | ||
|
|
7eefee1ea5 | ||
|
|
140c36a416 | ||
| f255ea389b | |||
| 0d343b9877 | |||
| df06100c32 | |||
|
|
ac5ddfc6f2 | ||
|
|
89b3f30466 | ||
|
|
3cb5cc096b | ||
|
|
5df560154f | ||
|
|
c1aa522e6c | ||
| a082469a01 | |||
|
|
3544d7278d | ||
|
|
d3110506ea | ||
| 8609f8458d | |||
| 8f5c34bc5f | |||
| b0d61b95ea | |||
| af7bd236a3 | |||
| d719338bcc | |||
| 6ddf2d1012 | |||
|
|
1b2d4b623a | ||
|
|
16d5c917d2 | ||
| 5976a4995e | |||
| dcd0cc4c20 | |||
| b3ca6c9d91 | |||
| e9d800f601 | |||
| b939a5e592 | |||
| aa62037fae | |||
| 722020ea86 | |||
| 96aa3f4a54 | |||
| c0c5f9842b | |||
| be27ca1855 | |||
| 92e4570672 | |||
| 820ae727ed | |||
| dbeb1c6b4b | |||
| 573e4b206a | |||
| abc05d426e | |||
| 2ea7479d75 | |||
| 9ac9713172 | |||
| 41dad3254d | |||
| 485eac59a0 | |||
|
|
73fc32b75d | ||
|
|
3d8e40e92b | ||
| 38e67f3533 | |||
| 7f63ee7c80 | |||
| 6a47f0d3e7 | |||
| fc50a9d4c6 | |||
|
|
45f43ff363 | ||
|
|
7b1d4c4849 | ||
|
|
c1f2c3951a | ||
| 9d4f726c31 | |||
| 1d7f626645 | |||
| c5228ba7ec | |||
| 6e1fcd8dee | |||
| 5bb563d694 | |||
| a3951c9d66 | |||
| aa177a9b8c | |||
| 03cb4720b8 | |||
|
|
0e65431f43 | ||
| 297c5a2dbb | |||
|
|
92b9c9334c | ||
|
|
706182ca0c | ||
|
|
68e0fc4976 | ||
| 504056eb90 | |||
| 5a1007c49c | |||
|
|
cbc14e21ec | ||
| ef3bfcdbd2 | |||
| ec1f27bab1 | |||
| 01c33069c4 | |||
| c637d39dc9 | |||
| 3e90bafbd1 | |||
|
|
d2c3e5db05 | ||
|
|
e824fcce2e | ||
|
|
f2c49872a6 | ||
|
|
229d9184b2 | ||
|
|
29908b77e3 | ||
|
|
3e02b3924a | ||
|
|
16cad04e5c | ||
|
|
e4f859a116 | ||
|
|
7f17a3d9c7 | ||
|
|
2d4d9691ca | ||
|
|
63575b36ed | ||
|
|
2eb46367bc | ||
|
|
cea0456148 | ||
|
|
6f5db13a49 | ||
|
|
068662625d | ||
|
|
23627835f9 | ||
|
|
f1ba6f9231 | ||
|
|
137fce3e30 | ||
|
|
7166dadbc0 | ||
|
|
bc274bdf7f | ||
|
|
082f8c0126 | ||
|
|
fd09c7e426 | ||
|
|
be40643379 | ||
|
|
835a270e65 | ||
|
|
8b03789941 | ||
|
|
b4a6b99301 | ||
|
|
13682a1930 | ||
|
|
669a66c24c | ||
|
|
13505b539e | ||
|
|
07ac340733 | ||
|
|
ba2b2fc543 | ||
| 21184e7625 | |||
| 8d1511e38f | |||
|
|
b18112b869 | ||
|
|
a228a9b1c0 | ||
|
|
1560ff0829 | ||
|
|
e839997f91 | ||
|
|
d8d054a0e1 | ||
|
|
efc720e47f | ||
|
|
0a85bea533 | ||
| 7de4125eb7 | |||
|
|
81d4f0c762 | ||
| 4c1b4fe651 | |||
|
|
e63541ef53 | ||
| 0bfc18c385 | |||
|
|
35f5df6b6b | ||
|
|
0f1ac2b230 | ||
| 3c0bdeaed3 | |||
| 11f2527b04 | |||
| 5d8175aeeb | |||
| b6b95cb0d0 | |||
| 655c5188a4 | |||
| 8b7451330f | |||
| b8fbc3f7a6 | |||
| 92dadba1cb | |||
| 3a6f585de0 | |||
|
|
47501ae917 | ||
|
|
28634839ec | ||
|
|
988244b7ae | ||
|
|
4b355a5448 | ||
| 1b7c96ed9b | |||
| 41365fab8f | |||
|
|
b511f9cd24 | ||
|
|
579cecbe6e | ||
| 5cc42be58a | |||
| 3d1a2eeb8d | |||
| 7b0ee2e44e | |||
| ac018997e8 | |||
| 6f449e9c1f | |||
| 543599a6a1 |
@@ -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",
|
||||||
|
|||||||
6
.gitignore
vendored
@@ -51,6 +51,8 @@ vendor/
|
|||||||
# Build logs
|
# Build logs
|
||||||
build_logs/
|
build_logs/
|
||||||
|
|
||||||
android/app/src/main/assets/public
|
# PWA icon files generated by capacitor-assets
|
||||||
android/app/src/main/res
|
icons
|
||||||
|
|
||||||
|
|
||||||
|
android/app/src/main/res/
|
||||||
100
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
|
||||||
@@ -54,6 +41,7 @@ Install dependencies:
|
|||||||
1. Run the production build:
|
1. Run the production build:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
rm -rf dist
|
||||||
npm run build:web
|
npm run build:web
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -75,16 +63,18 @@ Install dependencies:
|
|||||||
|
|
||||||
* Update the ClickUp tasks & CHANGELOG.md & the version in package.json, run `npm install`.
|
* Update the ClickUp tasks & CHANGELOG.md & the version in package.json, run `npm install`.
|
||||||
|
|
||||||
|
* Run a build to make sure package-lock version is updated, linting works, etc: `npm install && npm run build`
|
||||||
|
|
||||||
* Commit everything (since the commit hash is used the app).
|
* Commit everything (since the commit hash is used the app).
|
||||||
|
|
||||||
* Put the commit hash in the changelog (which will help you remember to bump the version later).
|
* Put the commit hash in the changelog (which will help you remember to bump the version in the step later).
|
||||||
|
|
||||||
* Tag with the new version, [online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or `git tag 0.3.55 && git push origin 0.3.55`.
|
* Tag with the new version, [online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or `git tag 1.0.2 && git push origin 1.0.2`.
|
||||||
|
|
||||||
* 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:web
|
||||||
```
|
```
|
||||||
|
|
||||||
... and transfer to the test server:
|
... and transfer to the test server:
|
||||||
@@ -103,13 +93,13 @@ TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.
|
|||||||
|
|
||||||
* `pkgx +npm sh`
|
* `pkgx +npm sh`
|
||||||
|
|
||||||
* `cd crowd-funder-for-time-pwa && git checkout master && git pull && git checkout 0.3.55 && npm install && npm run build && cd -`
|
* `cd crowd-funder-for-time-pwa && git checkout master && git pull && git checkout 1.0.2 && npm install && npm run build:web && cd -`
|
||||||
|
|
||||||
(The plain `npm run build` uses the .env.production file.)
|
(The plain `npm run build:web` uses the .env.production file.)
|
||||||
|
|
||||||
* Back up the time-safari/dist folder & deploy: `mv time-safari/dist time-safari-dist-prev.0 && mv crowd-funder-for-time-pwa/dist time-safari/`
|
* Back up the time-safari/dist folder & deploy: `mv time-safari/dist time-safari-dist-prev-2 && mv crowd-funder-for-time-pwa/dist time-safari/`
|
||||||
|
|
||||||
* Record the new hash in the changelog. Edit package.json to increment version & add "-beta", `npm install`, and commit. Also record what version is on production.
|
* Record the new hash in the changelog. Edit package.json to increment version & add "-beta", `npm install`, commit, and push. Also record what version is on production.
|
||||||
|
|
||||||
## Docker Deployment
|
## Docker Deployment
|
||||||
|
|
||||||
@@ -326,17 +316,33 @@ npm run build:electron-prod && npm run electron:start
|
|||||||
|
|
||||||
Prerequisites: macOS with Xcode installed
|
Prerequisites: macOS with Xcode installed
|
||||||
|
|
||||||
1. Build the web assets:
|
#### 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 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
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Build the web assets & update ios:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
rm -rf dist
|
rm -rf dist
|
||||||
npm run build:web
|
npm run build:web
|
||||||
npm run build:capacitor
|
npm run build:capacitor
|
||||||
```
|
|
||||||
|
|
||||||
2. Update iOS project with latest build:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx cap sync ios
|
npx cap sync ios
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -345,20 +351,20 @@ Prerequisites: macOS with Xcode installed
|
|||||||
3. Copy the assets:
|
3. Copy the assets:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# It makes no sense why capacitor-assets will not run without these but it actually changes the contents.
|
||||||
mkdir -p ios/App/App/Assets.xcassets/AppIcon.appiconset
|
mkdir -p ios/App/App/Assets.xcassets/AppIcon.appiconset
|
||||||
|
echo '{"images":[]}' > ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json
|
||||||
|
mkdir -p ios/App/App/Assets.xcassets/Splash.imageset
|
||||||
|
echo '{"images":[]}' > ios/App/App/Assets.xcassets/Splash.imageset/Contents.json
|
||||||
npx capacitor-assets generate --ios
|
npx capacitor-assets generate --ios
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Bump the version to match Android:
|
4. Bump the version to match Android & package.json:
|
||||||
|
|
||||||
```
|
```
|
||||||
cd ios/App
|
cd ios/App && xcrun agvtool new-version 35 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.0.2;/g" App.xcodeproj/project.pbxproj && cd -
|
||||||
xcrun agvtool new-version 15
|
|
||||||
# Unfortunately this edits Info.plist directly.
|
# Unfortunately this edits Info.plist directly.
|
||||||
#xcrun agvtool new-marketing-version 0.4.5
|
#xcrun agvtool new-marketing-version 0.4.5
|
||||||
cat App.xcodeproj/project.pbxproj | sed "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 0.4.5;/g" > temp
|
|
||||||
mv temp App.xcodeproj/project.pbxproj
|
|
||||||
cd -
|
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Open the project in Xcode:
|
5. Open the project in Xcode:
|
||||||
@@ -369,28 +375,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:
|
||||||
|
|
||||||
@@ -412,7 +415,7 @@ Prerequisites: Android Studio with SDK installed
|
|||||||
npx capacitor-assets generate --android
|
npx capacitor-assets generate --android
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Bump version to match iOS: android/app/build.gradle
|
4. Bump version to match iOS & package.json: android/app/build.gradle
|
||||||
|
|
||||||
5. Open the project in Android Studio:
|
5. Open the project in Android Studio:
|
||||||
|
|
||||||
@@ -429,7 +432,6 @@ Prerequisites: Android Studio with SDK installed
|
|||||||
./gradlew clean
|
./gradlew clean
|
||||||
./gradlew build -Dlint.baselines.continue=true
|
./gradlew build -Dlint.baselines.continue=true
|
||||||
cd -
|
cd -
|
||||||
npx cap run android
|
|
||||||
```
|
```
|
||||||
|
|
||||||
... or, to create the `aab` file, `bundle` instead of `build`:
|
... or, to create the `aab` file, `bundle` instead of `build`:
|
||||||
@@ -445,7 +447,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
|
||||||
@@ -458,8 +462,10 @@ 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
|
|
||||||
|
## Android Configuration for deep links
|
||||||
|
|
||||||
You must add the following intent filter to the `android/app/src/main/AndroidManifest.xml` file:
|
You must add the following intent filter to the `android/app/src/main/AndroidManifest.xml` file:
|
||||||
|
|
||||||
@@ -470,4 +476,6 @@ You must add the following intent filter to the `android/app/src/main/AndroidMan
|
|||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
<data android:scheme="timesafari" />
|
<data android:scheme="timesafari" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
... though when we tried that most recently it failed to 'build' the APK with: http(s) scheme and host attribute are missing, but are required for Android App Links [AppLinkUrlError]
|
||||||
|
|||||||
29
CHANGELOG.md
@@ -6,6 +6,35 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
|
||||||
|
## [1.0.2] - 2025.06.20 - 276e0a741bc327de3380c4e508cccb7fee58c06d
|
||||||
|
### Added
|
||||||
|
- Version on feed title
|
||||||
|
|
||||||
|
|
||||||
|
## [1.0.1] - 2025.06.20
|
||||||
|
### Added
|
||||||
|
- Allow a user to block someone else's content from view
|
||||||
|
|
||||||
|
|
||||||
|
## [1.0.0] - 2025.06.20 - 5aa693de6337e5dbb278bfddc6bd39094bc14f73
|
||||||
|
### Added
|
||||||
|
- Web-oriented migration from IndexedDB to SQLite
|
||||||
|
|
||||||
|
|
||||||
|
## [0.5.8]
|
||||||
|
### Added
|
||||||
|
- /deep-link/ path for URLs that are shared with people
|
||||||
|
### Changed
|
||||||
|
- External links now go to /deep-link/...
|
||||||
|
- Feed visuals now have arrow imagery from giver to receiver
|
||||||
|
|
||||||
|
|
||||||
|
## [0.4.7]
|
||||||
|
### Fixed
|
||||||
|
- Cameras everywhere
|
||||||
|
### Changed
|
||||||
|
- IndexedDB -> SQLite
|
||||||
|
|
||||||
|
|
||||||
## [0.4.5] - 2025.02.23
|
## [0.4.5] - 2025.02.23
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
47
README.md
@@ -3,6 +3,32 @@
|
|||||||
[Time Safari](https://timesafari.org/) allows people to ease into collaboration: start with expressions of gratitude
|
[Time Safari](https://timesafari.org/) allows people to ease into collaboration: start with expressions of gratitude
|
||||||
and expand to crowd-fund with time & money, then record and see the impact of contributions.
|
and expand to crowd-fund with time & money, then record and see the impact of contributions.
|
||||||
|
|
||||||
|
## Database Migration Status
|
||||||
|
|
||||||
|
**Current Status**: The application is undergoing a migration from Dexie (IndexedDB) to SQLite using absurd-sql. This migration is in **Phase 2** with a well-defined migration fence in place.
|
||||||
|
|
||||||
|
### Migration Progress
|
||||||
|
- ✅ **SQLite Database Service**: Fully implemented with absurd-sql
|
||||||
|
- ✅ **Platform Service Layer**: Unified database interface across platforms
|
||||||
|
- ✅ **Settings Migration**: Core user settings transferred
|
||||||
|
- ✅ **Account Migration**: Identity and key management
|
||||||
|
- 🔄 **Contact Migration**: User contact data (via import interface)
|
||||||
|
- 📋 **Code Cleanup**: Remove unused Dexie imports
|
||||||
|
|
||||||
|
### Migration Fence
|
||||||
|
The migration is controlled by a **migration fence** that separates legacy Dexie code from the new SQLite implementation. See [Migration Fence Definition](doc/migration-fence-definition.md) for complete details.
|
||||||
|
|
||||||
|
**Key Points**:
|
||||||
|
- Legacy Dexie database is disabled by default (`USE_DEXIE_DB = false`)
|
||||||
|
- All database operations go through `PlatformService`
|
||||||
|
- Migration tools provide controlled access to both databases
|
||||||
|
- Clear separation between legacy and new code
|
||||||
|
|
||||||
|
### Migration Documentation
|
||||||
|
- [Migration Guide](doc/migration-to-wa-sqlite.md) - Complete migration process
|
||||||
|
- [Migration Fence Definition](doc/migration-fence-definition.md) - Fence boundaries and rules
|
||||||
|
- [Database Migration Guide](doc/database-migration-guide.md) - User-facing migration tools
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
See [project.task.yaml](project.task.yaml) for current priorities.
|
See [project.task.yaml](project.task.yaml) for current priorities.
|
||||||
@@ -21,16 +47,10 @@ npm run dev
|
|||||||
|
|
||||||
See [BUILDING.md](BUILDING.md) for more details.
|
See [BUILDING.md](BUILDING.md) for more details.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
See [TESTING.md](test-playwright/TESTING.md) for detailed test instructions.
|
See [TESTING.md](test-playwright/TESTING.md) for detailed test instructions.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Icons
|
## Icons
|
||||||
|
|
||||||
Application icons are in the `assets` directory, processed by the `capacitor-assets` command.
|
Application icons are in the `assets` directory, processed by the `capacitor-assets` command.
|
||||||
@@ -66,6 +86,21 @@ Key principles:
|
|||||||
- Common interfaces are shared through `common.ts`
|
- Common interfaces are shared through `common.ts`
|
||||||
- Type definitions are generated from Zod schemas where possible
|
- Type definitions are generated from Zod schemas where possible
|
||||||
|
|
||||||
|
### Database Architecture
|
||||||
|
|
||||||
|
The application uses a platform-agnostic database layer:
|
||||||
|
|
||||||
|
* `src/services/PlatformService.ts` - Database interface definition
|
||||||
|
* `src/services/PlatformServiceFactory.ts` - Platform-specific service factory
|
||||||
|
* `src/services/AbsurdSqlDatabaseService.ts` - SQLite implementation
|
||||||
|
* `src/db/` - Legacy Dexie database (migration in progress)
|
||||||
|
|
||||||
|
**Development Guidelines**:
|
||||||
|
- Always use `PlatformService` for database operations
|
||||||
|
- Never import Dexie directly in application code
|
||||||
|
- Test with `USE_DEXIE_DB = false` for new features
|
||||||
|
- Use migration tools for data transfer between systems
|
||||||
|
|
||||||
### Kudos
|
### Kudos
|
||||||
|
|
||||||
Gifts make the world go 'round!
|
Gifts make the world go 'round!
|
||||||
|
|||||||
@@ -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 35
|
||||||
versionName "0.4.7"
|
versionName "1.0.2"
|
||||||
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')
|
||||||
|
|||||||
@@ -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 {}
|
|
||||||
|
Before Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
@@ -1,5 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background>
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
<inset android:drawable="@mipmap/ic_launcher_background" android:inset="16.7%" />
|
||||||
|
</background>
|
||||||
|
<foreground>
|
||||||
|
<inset android:drawable="@mipmap/ic_launcher_foreground" android:inset="16.7%" />
|
||||||
|
</foreground>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
<background>
|
||||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
<inset android:drawable="@mipmap/ic_launcher_background" android:inset="16.7%" />
|
||||||
|
</background>
|
||||||
|
<foreground>
|
||||||
|
<inset android:drawable="@mipmap/ic_launcher_foreground" android:inset="16.7%" />
|
||||||
|
</foreground>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 23 KiB |
@@ -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')
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 60 KiB |
BIN
assets/icon.png
Normal file
|
After Width: | Height: | Size: 279 KiB |
BIN
assets/splash-dark.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
assets/splash.png
Normal file
|
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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ try {
|
|||||||
- `src/interfaces/deepLinks.ts`: Type definitions and validation schemas
|
- `src/interfaces/deepLinks.ts`: Type definitions and validation schemas
|
||||||
- `src/services/deepLinks.ts`: Deep link processing service
|
- `src/services/deepLinks.ts`: Deep link processing service
|
||||||
- `src/main.capacitor.ts`: Capacitor integration
|
- `src/main.capacitor.ts`: Capacitor integration
|
||||||
|
- `src/views/DeepLinkRedirectView.vue`: Page to handle links to both mobile and web
|
||||||
|
|
||||||
## Type Safety Examples
|
## Type Safety Examples
|
||||||
|
|
||||||
|
|||||||
295
doc/database-migration-guide.md
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
# Database Migration Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The Database Migration feature allows you to compare and migrate data between Dexie (IndexedDB) and SQLite databases in the TimeSafari application. This is particularly useful during the transition from the old Dexie-based storage system to the new SQLite-based system.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### 1. Database Comparison
|
||||||
|
|
||||||
|
- Compare data between Dexie and SQLite databases
|
||||||
|
- View detailed differences in contacts and settings
|
||||||
|
- Identify added, modified, and missing records
|
||||||
|
- Export comparison results for analysis
|
||||||
|
|
||||||
|
### 2. Data Migration
|
||||||
|
|
||||||
|
- Migrate contacts from Dexie to SQLite
|
||||||
|
- Migrate settings from Dexie to SQLite
|
||||||
|
- Option to overwrite existing records or skip them
|
||||||
|
- Comprehensive error handling and reporting
|
||||||
|
|
||||||
|
### 3. User Interface
|
||||||
|
|
||||||
|
- Modern, responsive UI built with Tailwind CSS
|
||||||
|
- Real-time loading states and progress indicators
|
||||||
|
- Clear success and error messaging
|
||||||
|
- Export functionality for comparison data
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
### Enable Dexie Database
|
||||||
|
|
||||||
|
Before using the migration features, you must enable the Dexie database by setting:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In constants/app.ts
|
||||||
|
export const USE_DEXIE_DB = true;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: This should only be enabled temporarily during migration. Remember to set it back to `false` after migration is complete.
|
||||||
|
|
||||||
|
## Accessing the Migration Interface
|
||||||
|
|
||||||
|
1. Navigate to the **Account** page in the TimeSafari app
|
||||||
|
2. Scroll down to find the **Database Migration** link
|
||||||
|
3. Click the link to open the migration interface
|
||||||
|
|
||||||
|
## Using the Migration Interface
|
||||||
|
|
||||||
|
### Step 1: Compare Databases
|
||||||
|
|
||||||
|
1. Click the **"Compare Databases"** button
|
||||||
|
2. The system will retrieve data from both Dexie and SQLite databases
|
||||||
|
3. Review the comparison results showing:
|
||||||
|
- Summary counts for each database
|
||||||
|
- Detailed differences (added, modified, missing records)
|
||||||
|
- Specific records that need attention
|
||||||
|
|
||||||
|
### Step 2: Review Differences
|
||||||
|
|
||||||
|
The comparison results are displayed in several sections:
|
||||||
|
|
||||||
|
#### Summary Cards
|
||||||
|
|
||||||
|
- **Dexie Contacts**: Number of contacts in Dexie database
|
||||||
|
- **SQLite Contacts**: Number of contacts in SQLite database
|
||||||
|
- **Dexie Settings**: Number of settings in Dexie database
|
||||||
|
- **SQLite Settings**: Number of settings in SQLite database
|
||||||
|
|
||||||
|
#### Contact Differences
|
||||||
|
|
||||||
|
- **Added**: Contacts in Dexie but not in SQLite
|
||||||
|
- **Modified**: Contacts that differ between databases
|
||||||
|
- **Missing**: Contacts in SQLite but not in Dexie
|
||||||
|
|
||||||
|
#### Settings Differences
|
||||||
|
|
||||||
|
- **Added**: Settings in Dexie but not in SQLite
|
||||||
|
- **Modified**: Settings that differ between databases
|
||||||
|
- **Missing**: Settings in SQLite but not in Dexie
|
||||||
|
|
||||||
|
### Step 3: Configure Migration Options
|
||||||
|
|
||||||
|
Before migrating data, configure the migration options:
|
||||||
|
|
||||||
|
- **Overwrite existing records**: When enabled, existing records in SQLite will be updated with data from Dexie. When disabled, existing records will be skipped.
|
||||||
|
|
||||||
|
### Step 4: Migrate Data
|
||||||
|
|
||||||
|
#### Migrate Contacts
|
||||||
|
|
||||||
|
1. Click the **"Migrate Contacts"** button
|
||||||
|
2. The system will transfer contacts from Dexie to SQLite
|
||||||
|
3. Review the migration results showing:
|
||||||
|
- Number of contacts successfully migrated
|
||||||
|
- Any warnings or errors encountered
|
||||||
|
|
||||||
|
#### Migrate Settings
|
||||||
|
|
||||||
|
1. Click the **"Migrate Settings"** button
|
||||||
|
2. The system will transfer settings from Dexie to SQLite
|
||||||
|
3. Review the migration results showing:
|
||||||
|
- Number of settings successfully migrated
|
||||||
|
- Any warnings or errors encountered
|
||||||
|
|
||||||
|
### Step 5: Export Comparison (Optional)
|
||||||
|
|
||||||
|
1. Click the **"Export Comparison"** button
|
||||||
|
2. A JSON file will be downloaded containing the complete comparison data
|
||||||
|
3. This file can be used for analysis or backup purposes
|
||||||
|
|
||||||
|
## Migration Process Details
|
||||||
|
|
||||||
|
### Contact Migration
|
||||||
|
|
||||||
|
The contact migration process:
|
||||||
|
|
||||||
|
1. **Retrieves** all contacts from Dexie database
|
||||||
|
2. **Checks** for existing contacts in SQLite by DID
|
||||||
|
3. **Inserts** new contacts or **updates** existing ones (if overwrite is enabled)
|
||||||
|
4. **Handles** complex fields like `contactMethods` (JSON arrays)
|
||||||
|
5. **Reports** success/failure for each contact
|
||||||
|
|
||||||
|
### Settings Migration
|
||||||
|
|
||||||
|
The settings migration process:
|
||||||
|
|
||||||
|
1. **Retrieves** all settings from Dexie database
|
||||||
|
2. **Focuses** on key user-facing settings:
|
||||||
|
- `firstName`
|
||||||
|
- `isRegistered`
|
||||||
|
- `profileImageUrl`
|
||||||
|
- `showShortcutBvc`
|
||||||
|
- `searchBoxes`
|
||||||
|
3. **Preserves** other settings in SQLite
|
||||||
|
4. **Reports** success/failure for each setting
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
#### Dexie Database Not Enabled
|
||||||
|
|
||||||
|
**Error**: "Dexie database is not enabled"
|
||||||
|
**Solution**: Set `USE_DEXIE_DB = true` in `constants/app.ts`
|
||||||
|
|
||||||
|
#### Database Connection Issues
|
||||||
|
|
||||||
|
**Error**: "Failed to retrieve Dexie contacts"
|
||||||
|
**Solution**: Check that the Dexie database is properly initialized and accessible
|
||||||
|
|
||||||
|
#### SQLite Query Errors
|
||||||
|
|
||||||
|
**Error**: "Failed to retrieve SQLite contacts"
|
||||||
|
**Solution**: Verify that the SQLite database is properly set up and the platform service is working
|
||||||
|
|
||||||
|
#### Migration Failures
|
||||||
|
|
||||||
|
**Error**: "Migration failed: [specific error]"
|
||||||
|
**Solution**: Review the error details and check data integrity in both databases
|
||||||
|
|
||||||
|
### Error Recovery
|
||||||
|
|
||||||
|
1. **Review** the error messages carefully
|
||||||
|
2. **Check** the browser console for additional details
|
||||||
|
3. **Verify** database connectivity and permissions
|
||||||
|
4. **Retry** the operation if appropriate
|
||||||
|
5. **Export** comparison data for manual review if needed
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Before Migration
|
||||||
|
|
||||||
|
1. **Backup** your data if possible
|
||||||
|
2. **Test** the migration on a small dataset first
|
||||||
|
3. **Verify** that both databases are accessible
|
||||||
|
4. **Review** the comparison results before migrating
|
||||||
|
|
||||||
|
### During Migration
|
||||||
|
|
||||||
|
1. **Don't** interrupt the migration process
|
||||||
|
2. **Monitor** the progress and error messages
|
||||||
|
3. **Note** any warnings or skipped records
|
||||||
|
4. **Export** comparison data for reference
|
||||||
|
|
||||||
|
### After Migration
|
||||||
|
|
||||||
|
1. **Verify** that data was migrated correctly
|
||||||
|
2. **Test** the application functionality
|
||||||
|
3. **Disable** Dexie database (`USE_DEXIE_DB = false`)
|
||||||
|
4. **Clean up** any temporary files or exports
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### Database Schema
|
||||||
|
|
||||||
|
The migration handles the following data structures:
|
||||||
|
|
||||||
|
#### Contacts Table
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Contact {
|
||||||
|
did: string; // Decentralized Identifier
|
||||||
|
name: string; // Contact name
|
||||||
|
contactMethods: ContactMethod[]; // Array of contact methods
|
||||||
|
nextPubKeyHashB64: string; // Next public key hash
|
||||||
|
notes: string; // Contact notes
|
||||||
|
profileImageUrl: string; // Profile image URL
|
||||||
|
publicKeyBase64: string; // Public key in base64
|
||||||
|
seesMe: boolean; // Visibility flag
|
||||||
|
registered: boolean; // Registration status
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Settings Table
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface Settings {
|
||||||
|
id: number; // Settings ID
|
||||||
|
accountDid: string; // Account DID
|
||||||
|
activeDid: string; // Active DID
|
||||||
|
firstName: string; // User's first name
|
||||||
|
isRegistered: boolean; // Registration status
|
||||||
|
profileImageUrl: string; // Profile image URL
|
||||||
|
showShortcutBvc: boolean; // UI preference
|
||||||
|
searchBoxes: any[]; // Search configuration
|
||||||
|
// ... other fields
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration Logic
|
||||||
|
|
||||||
|
The migration service uses sophisticated comparison logic:
|
||||||
|
|
||||||
|
1. **Primary Key Matching**: Uses DID for contacts, ID for settings
|
||||||
|
2. **Deep Comparison**: Compares all fields including complex objects
|
||||||
|
3. **JSON Handling**: Properly handles JSON fields like `contactMethods` and `searchBoxes`
|
||||||
|
4. **Conflict Resolution**: Provides options for handling existing records
|
||||||
|
|
||||||
|
### Performance Considerations
|
||||||
|
|
||||||
|
- **Batch Processing**: Processes records one by one for reliability
|
||||||
|
- **Error Isolation**: Individual record failures don't stop the entire migration
|
||||||
|
- **Memory Management**: Handles large datasets efficiently
|
||||||
|
- **Progress Reporting**: Provides real-time feedback during migration
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Migration Stuck
|
||||||
|
|
||||||
|
If the migration appears to be stuck:
|
||||||
|
|
||||||
|
1. **Check** the browser console for errors
|
||||||
|
2. **Refresh** the page and try again
|
||||||
|
3. **Verify** database connectivity
|
||||||
|
4. **Check** for large datasets that might take time
|
||||||
|
|
||||||
|
### Incomplete Migration
|
||||||
|
|
||||||
|
If migration doesn't complete:
|
||||||
|
|
||||||
|
1. **Review** error messages
|
||||||
|
2. **Check** data integrity in both databases
|
||||||
|
3. **Export** comparison data for manual review
|
||||||
|
4. **Consider** migrating in smaller batches
|
||||||
|
|
||||||
|
### Data Inconsistencies
|
||||||
|
|
||||||
|
If you notice data inconsistencies:
|
||||||
|
|
||||||
|
1. **Export** comparison data
|
||||||
|
2. **Review** the differences carefully
|
||||||
|
3. **Manually** verify critical records
|
||||||
|
4. **Consider** selective migration of specific records
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues with the Database Migration feature:
|
||||||
|
|
||||||
|
1. **Check** this documentation first
|
||||||
|
2. **Review** the browser console for error details
|
||||||
|
3. **Export** comparison data for analysis
|
||||||
|
4. **Contact** the development team with specific error details
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- **Data Privacy**: Migration data is processed locally and not sent to external servers
|
||||||
|
- **Access Control**: Only users with access to the account can perform migration
|
||||||
|
- **Data Integrity**: Migration preserves data integrity and handles conflicts gracefully
|
||||||
|
- **Audit Trail**: Export functionality provides an audit trail of migration operations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Note**: This migration tool is designed for the transition period between database systems. Once migration is complete and verified, the Dexie database should be disabled to avoid confusion and potential data conflicts.
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
## Schema Mapping
|
## Schema Mapping
|
||||||
|
|
||||||
### Current Dexie Schema
|
### Current Dexie Schema
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Current Dexie schema
|
// Current Dexie schema
|
||||||
const db = new Dexie('TimeSafariDB');
|
const db = new Dexie('TimeSafariDB');
|
||||||
@@ -15,6 +16,7 @@ db.version(1).stores({
|
|||||||
```
|
```
|
||||||
|
|
||||||
### New SQLite Schema
|
### New SQLite Schema
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
-- New SQLite schema
|
-- New SQLite schema
|
||||||
CREATE TABLE accounts (
|
CREATE TABLE accounts (
|
||||||
@@ -50,6 +52,7 @@ CREATE INDEX idx_settings_updated_at ON settings(updated_at);
|
|||||||
### 1. Account Operations
|
### 1. Account Operations
|
||||||
|
|
||||||
#### Get Account by DID
|
#### Get Account by DID
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Dexie
|
// Dexie
|
||||||
const account = await db.accounts.get(did);
|
const account = await db.accounts.get(did);
|
||||||
@@ -62,6 +65,7 @@ const account = result[0]?.values[0];
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### Get All Accounts
|
#### Get All Accounts
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Dexie
|
// Dexie
|
||||||
const accounts = await db.accounts.toArray();
|
const accounts = await db.accounts.toArray();
|
||||||
@@ -74,6 +78,7 @@ const accounts = result[0]?.values || [];
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### Add Account
|
#### Add Account
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Dexie
|
// Dexie
|
||||||
await db.accounts.add({
|
await db.accounts.add({
|
||||||
@@ -91,6 +96,7 @@ await db.run(`
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### Update Account
|
#### Update Account
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Dexie
|
// Dexie
|
||||||
await db.accounts.update(did, {
|
await db.accounts.update(did, {
|
||||||
@@ -100,7 +106,7 @@ await db.accounts.update(did, {
|
|||||||
|
|
||||||
// absurd-sql
|
// absurd-sql
|
||||||
await db.run(`
|
await db.run(`
|
||||||
UPDATE accounts
|
UPDATE accounts
|
||||||
SET public_key_hex = ?, updated_at = ?
|
SET public_key_hex = ?, updated_at = ?
|
||||||
WHERE did = ?
|
WHERE did = ?
|
||||||
`, [publicKeyHex, Date.now(), did]);
|
`, [publicKeyHex, Date.now(), did]);
|
||||||
@@ -109,6 +115,7 @@ await db.run(`
|
|||||||
### 2. Settings Operations
|
### 2. Settings Operations
|
||||||
|
|
||||||
#### Get Setting
|
#### Get Setting
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Dexie
|
// Dexie
|
||||||
const setting = await db.settings.get(key);
|
const setting = await db.settings.get(key);
|
||||||
@@ -121,6 +128,7 @@ const setting = result[0]?.values[0];
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### Set Setting
|
#### Set Setting
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Dexie
|
// Dexie
|
||||||
await db.settings.put({
|
await db.settings.put({
|
||||||
@@ -142,6 +150,7 @@ await db.run(`
|
|||||||
### 3. Contact Operations
|
### 3. Contact Operations
|
||||||
|
|
||||||
#### Get Contacts by Account
|
#### Get Contacts by Account
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Dexie
|
// Dexie
|
||||||
const contacts = await db.contacts
|
const contacts = await db.contacts
|
||||||
@@ -151,7 +160,7 @@ const contacts = await db.contacts
|
|||||||
|
|
||||||
// absurd-sql
|
// absurd-sql
|
||||||
const result = await db.exec(`
|
const result = await db.exec(`
|
||||||
SELECT * FROM contacts
|
SELECT * FROM contacts
|
||||||
WHERE did = ?
|
WHERE did = ?
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
`, [accountDid]);
|
`, [accountDid]);
|
||||||
@@ -159,6 +168,7 @@ const contacts = result[0]?.values || [];
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### Add Contact
|
#### Add Contact
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Dexie
|
// Dexie
|
||||||
await db.contacts.add({
|
await db.contacts.add({
|
||||||
@@ -179,6 +189,7 @@ await db.run(`
|
|||||||
## Transaction Mapping
|
## Transaction Mapping
|
||||||
|
|
||||||
### Batch Operations
|
### Batch Operations
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Dexie
|
// Dexie
|
||||||
await db.transaction('rw', [db.accounts, db.contacts], async () => {
|
await db.transaction('rw', [db.accounts, db.contacts], async () => {
|
||||||
@@ -210,10 +221,11 @@ try {
|
|||||||
## Migration Helper Functions
|
## Migration Helper Functions
|
||||||
|
|
||||||
### 1. Data Export (Dexie to JSON)
|
### 1. Data Export (Dexie to JSON)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
async function exportDexieData(): Promise<MigrationData> {
|
async function exportDexieData(): Promise<MigrationData> {
|
||||||
const db = new Dexie('TimeSafariDB');
|
const db = new Dexie('TimeSafariDB');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accounts: await db.accounts.toArray(),
|
accounts: await db.accounts.toArray(),
|
||||||
settings: await db.settings.toArray(),
|
settings: await db.settings.toArray(),
|
||||||
@@ -228,6 +240,7 @@ async function exportDexieData(): Promise<MigrationData> {
|
|||||||
```
|
```
|
||||||
|
|
||||||
### 2. Data Import (JSON to absurd-sql)
|
### 2. Data Import (JSON to absurd-sql)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
async function importToAbsurdSql(data: MigrationData): Promise<void> {
|
async function importToAbsurdSql(data: MigrationData): Promise<void> {
|
||||||
await db.exec('BEGIN TRANSACTION;');
|
await db.exec('BEGIN TRANSACTION;');
|
||||||
@@ -239,7 +252,7 @@ async function importToAbsurdSql(data: MigrationData): Promise<void> {
|
|||||||
VALUES (?, ?, ?, ?)
|
VALUES (?, ?, ?, ?)
|
||||||
`, [account.did, account.publicKeyHex, account.createdAt, account.updatedAt]);
|
`, [account.did, account.publicKeyHex, account.createdAt, account.updatedAt]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import settings
|
// Import settings
|
||||||
for (const setting of data.settings) {
|
for (const setting of data.settings) {
|
||||||
await db.run(`
|
await db.run(`
|
||||||
@@ -247,7 +260,7 @@ async function importToAbsurdSql(data: MigrationData): Promise<void> {
|
|||||||
VALUES (?, ?, ?)
|
VALUES (?, ?, ?)
|
||||||
`, [setting.key, setting.value, setting.updatedAt]);
|
`, [setting.key, setting.value, setting.updatedAt]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import contacts
|
// Import contacts
|
||||||
for (const contact of data.contacts) {
|
for (const contact of data.contacts) {
|
||||||
await db.run(`
|
await db.run(`
|
||||||
@@ -264,6 +277,7 @@ async function importToAbsurdSql(data: MigrationData): Promise<void> {
|
|||||||
```
|
```
|
||||||
|
|
||||||
### 3. Verification
|
### 3. Verification
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
async function verifyMigration(dexieData: MigrationData): Promise<boolean> {
|
async function verifyMigration(dexieData: MigrationData): Promise<boolean> {
|
||||||
// Verify account count
|
// Verify account count
|
||||||
@@ -272,21 +286,21 @@ async function verifyMigration(dexieData: MigrationData): Promise<boolean> {
|
|||||||
if (accountCount !== dexieData.accounts.length) {
|
if (accountCount !== dexieData.accounts.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify settings count
|
// Verify settings count
|
||||||
const settingsResult = await db.exec('SELECT COUNT(*) as count FROM settings');
|
const settingsResult = await db.exec('SELECT COUNT(*) as count FROM settings');
|
||||||
const settingsCount = settingsResult[0].values[0][0];
|
const settingsCount = settingsResult[0].values[0][0];
|
||||||
if (settingsCount !== dexieData.settings.length) {
|
if (settingsCount !== dexieData.settings.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify contacts count
|
// Verify contacts count
|
||||||
const contactsResult = await db.exec('SELECT COUNT(*) as count FROM contacts');
|
const contactsResult = await db.exec('SELECT COUNT(*) as count FROM contacts');
|
||||||
const contactsCount = contactsResult[0].values[0][0];
|
const contactsCount = contactsResult[0].values[0][0];
|
||||||
if (contactsCount !== dexieData.contacts.length) {
|
if (contactsCount !== dexieData.contacts.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify data integrity
|
// Verify data integrity
|
||||||
for (const account of dexieData.accounts) {
|
for (const account of dexieData.accounts) {
|
||||||
const result = await db.exec(
|
const result = await db.exec(
|
||||||
@@ -294,12 +308,12 @@ async function verifyMigration(dexieData: MigrationData): Promise<boolean> {
|
|||||||
[account.did]
|
[account.did]
|
||||||
);
|
);
|
||||||
const migratedAccount = result[0]?.values[0];
|
const migratedAccount = result[0]?.values[0];
|
||||||
if (!migratedAccount ||
|
if (!migratedAccount ||
|
||||||
migratedAccount[1] !== account.publicKeyHex) { // public_key_hex is second column
|
migratedAccount[1] !== account.publicKeyHex) { // public_key_hex is second column
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -307,18 +321,21 @@ async function verifyMigration(dexieData: MigrationData): Promise<boolean> {
|
|||||||
## Performance Considerations
|
## Performance Considerations
|
||||||
|
|
||||||
### 1. Indexing
|
### 1. Indexing
|
||||||
|
|
||||||
- Dexie automatically creates indexes based on the schema
|
- Dexie automatically creates indexes based on the schema
|
||||||
- absurd-sql requires explicit index creation
|
- absurd-sql requires explicit index creation
|
||||||
- Added indexes for frequently queried fields
|
- Added indexes for frequently queried fields
|
||||||
- Use `PRAGMA journal_mode=MEMORY;` for better performance
|
- Use `PRAGMA journal_mode=MEMORY;` for better performance
|
||||||
|
|
||||||
### 2. Batch Operations
|
### 2. Batch Operations
|
||||||
|
|
||||||
- Dexie has built-in bulk operations
|
- Dexie has built-in bulk operations
|
||||||
- absurd-sql uses transactions for batch operations
|
- absurd-sql uses transactions for batch operations
|
||||||
- Consider chunking large datasets
|
- Consider chunking large datasets
|
||||||
- Use prepared statements for repeated queries
|
- Use prepared statements for repeated queries
|
||||||
|
|
||||||
### 3. Query Optimization
|
### 3. Query Optimization
|
||||||
|
|
||||||
- Dexie uses IndexedDB's native indexing
|
- Dexie uses IndexedDB's native indexing
|
||||||
- absurd-sql requires explicit query optimization
|
- absurd-sql requires explicit query optimization
|
||||||
- Use prepared statements for repeated queries
|
- Use prepared statements for repeated queries
|
||||||
@@ -327,6 +344,7 @@ async function verifyMigration(dexieData: MigrationData): Promise<boolean> {
|
|||||||
## Error Handling
|
## Error Handling
|
||||||
|
|
||||||
### 1. Common Errors
|
### 1. Common Errors
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Dexie errors
|
// Dexie errors
|
||||||
try {
|
try {
|
||||||
@@ -351,6 +369,7 @@ try {
|
|||||||
```
|
```
|
||||||
|
|
||||||
### 2. Transaction Recovery
|
### 2. Transaction Recovery
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Dexie transaction
|
// Dexie transaction
|
||||||
try {
|
try {
|
||||||
@@ -396,4 +415,4 @@ try {
|
|||||||
- Remove Dexie database
|
- Remove Dexie database
|
||||||
- Clear IndexedDB storage
|
- Clear IndexedDB storage
|
||||||
- Update application code
|
- Update application code
|
||||||
- Remove old dependencies
|
- Remove old dependencies
|
||||||
|
|||||||
272
doc/migration-fence-definition.md
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
# Migration Fence Definition: Dexie to SQLite
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document defines the **migration fence** - the boundary between the legacy Dexie (IndexedDB) storage system and the new SQLite-based storage system in TimeSafari. The fence ensures controlled migration while maintaining data integrity and application stability.
|
||||||
|
|
||||||
|
## Current Migration Status
|
||||||
|
|
||||||
|
### ✅ Completed Components
|
||||||
|
- **SQLite Database Service**: Fully implemented with absurd-sql
|
||||||
|
- **Platform Service Layer**: Unified database interface across platforms
|
||||||
|
- **Migration Tools**: Data comparison and transfer utilities
|
||||||
|
- **Schema Migration**: Complete table structure migration
|
||||||
|
- **Data Export/Import**: Backup and restore functionality
|
||||||
|
|
||||||
|
### 🔄 Active Migration Components
|
||||||
|
- **Settings Migration**: Core user settings transferred
|
||||||
|
- **Account Migration**: Identity and key management
|
||||||
|
- **Contact Migration**: User contact data (via import interface)
|
||||||
|
|
||||||
|
### ❌ Legacy Components (Fence Boundary)
|
||||||
|
- **Dexie Database**: Legacy IndexedDB storage (disabled by default)
|
||||||
|
- **Dexie-Specific Code**: Direct database access patterns
|
||||||
|
- **Legacy Migration Paths**: Old data transfer methods
|
||||||
|
|
||||||
|
## Migration Fence Definition
|
||||||
|
|
||||||
|
### 1. Configuration Boundary
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/constants/app.ts
|
||||||
|
export const USE_DEXIE_DB = false; // FENCE: Controls legacy database access
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fence Rule**: When `USE_DEXIE_DB = false`:
|
||||||
|
- All new data operations use SQLite
|
||||||
|
- Legacy Dexie database is not initialized
|
||||||
|
- Migration tools are the only path to legacy data
|
||||||
|
|
||||||
|
**Fence Rule**: When `USE_DEXIE_DB = true`:
|
||||||
|
- Legacy database is available for migration
|
||||||
|
- Dual-write operations may be enabled
|
||||||
|
- Migration tools can access both databases
|
||||||
|
|
||||||
|
### 2. Service Layer Boundary
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/services/PlatformServiceFactory.ts
|
||||||
|
export class PlatformServiceFactory {
|
||||||
|
public static getInstance(): PlatformService {
|
||||||
|
// FENCE: All database operations go through platform service
|
||||||
|
// No direct Dexie access outside migration tools
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fence Rule**: All database operations must use:
|
||||||
|
- `PlatformService.dbQuery()` for read operations
|
||||||
|
- `PlatformService.dbExec()` for write operations
|
||||||
|
- No direct `db.` or `accountsDBPromise` access in application code
|
||||||
|
|
||||||
|
### 3. Data Access Patterns
|
||||||
|
|
||||||
|
#### ✅ Allowed (Inside Fence)
|
||||||
|
```typescript
|
||||||
|
// Use platform service for all database operations
|
||||||
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
|
const contacts = await platformService.dbQuery(
|
||||||
|
"SELECT * FROM contacts WHERE did = ?",
|
||||||
|
[accountDid]
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ❌ Forbidden (Outside Fence)
|
||||||
|
```typescript
|
||||||
|
// Direct Dexie access (legacy pattern)
|
||||||
|
const contacts = await db.contacts.where('did').equals(accountDid).toArray();
|
||||||
|
|
||||||
|
// Direct database reference
|
||||||
|
const result = await accountsDBPromise;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Migration Tool Boundary
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/services/indexedDBMigrationService.ts
|
||||||
|
// FENCE: Only migration tools can access both databases
|
||||||
|
export async function compareDatabases(): Promise<DataComparison> {
|
||||||
|
// This is the ONLY place where both databases are accessed
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fence Rule**: Migration tools are the exclusive interface between:
|
||||||
|
- Legacy Dexie database
|
||||||
|
- New SQLite database
|
||||||
|
- Data comparison and transfer operations
|
||||||
|
|
||||||
|
## Migration Fence Guidelines
|
||||||
|
|
||||||
|
### 1. Code Development Rules
|
||||||
|
|
||||||
|
#### New Feature Development
|
||||||
|
- **Always** use `PlatformService` for database operations
|
||||||
|
- **Never** import or reference Dexie directly
|
||||||
|
- **Always** test with `USE_DEXIE_DB = false`
|
||||||
|
|
||||||
|
#### Legacy Code Maintenance
|
||||||
|
- **Only** modify Dexie code for migration purposes
|
||||||
|
- **Always** add migration tests for schema changes
|
||||||
|
- **Never** add new Dexie-specific features
|
||||||
|
|
||||||
|
### 2. Data Integrity Rules
|
||||||
|
|
||||||
|
#### Migration Safety
|
||||||
|
- **Always** create backups before migration
|
||||||
|
- **Always** verify data integrity after migration
|
||||||
|
- **Never** delete legacy data until verified
|
||||||
|
|
||||||
|
#### Rollback Strategy
|
||||||
|
- **Always** maintain ability to rollback to Dexie
|
||||||
|
- **Always** preserve migration logs
|
||||||
|
- **Never** assume migration is irreversible
|
||||||
|
|
||||||
|
### 3. Testing Requirements
|
||||||
|
|
||||||
|
#### Migration Testing
|
||||||
|
```typescript
|
||||||
|
// Required test pattern for migration
|
||||||
|
describe('Database Migration', () => {
|
||||||
|
it('should migrate data without loss', async () => {
|
||||||
|
// 1. Enable Dexie
|
||||||
|
// 2. Create test data
|
||||||
|
// 3. Run migration
|
||||||
|
// 4. Verify data integrity
|
||||||
|
// 5. Disable Dexie
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Application Testing
|
||||||
|
```typescript
|
||||||
|
// Required test pattern for application features
|
||||||
|
describe('Feature with Database', () => {
|
||||||
|
it('should work with SQLite only', async () => {
|
||||||
|
// Test with USE_DEXIE_DB = false
|
||||||
|
// Verify all operations use PlatformService
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Fence Enforcement
|
||||||
|
|
||||||
|
### 1. Static Analysis
|
||||||
|
|
||||||
|
#### ESLint Rules
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"rules": {
|
||||||
|
"no-restricted-imports": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"patterns": [
|
||||||
|
{
|
||||||
|
"group": ["../db/index"],
|
||||||
|
"message": "Use PlatformService instead of direct Dexie access"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### TypeScript Rules
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"noImplicitAny": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Runtime Checks
|
||||||
|
|
||||||
|
#### Development Mode Validation
|
||||||
|
```typescript
|
||||||
|
// Development-only fence validation
|
||||||
|
if (import.meta.env.DEV && USE_DEXIE_DB) {
|
||||||
|
console.warn('⚠️ Dexie is enabled - migration mode active');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Production Safety
|
||||||
|
```typescript
|
||||||
|
// Production fence enforcement
|
||||||
|
if (import.meta.env.PROD && USE_DEXIE_DB) {
|
||||||
|
throw new Error('Dexie cannot be enabled in production');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Fence Timeline
|
||||||
|
|
||||||
|
### Phase 1: Fence Establishment ✅
|
||||||
|
- [x] Define migration fence boundaries
|
||||||
|
- [x] Implement PlatformService layer
|
||||||
|
- [x] Create migration tools
|
||||||
|
- [x] Set `USE_DEXIE_DB = false` by default
|
||||||
|
|
||||||
|
### Phase 2: Data Migration 🔄
|
||||||
|
- [x] Migrate core settings
|
||||||
|
- [x] Migrate account data
|
||||||
|
- [ ] Complete contact migration
|
||||||
|
- [ ] Verify all data integrity
|
||||||
|
|
||||||
|
### Phase 3: Code Cleanup 📋
|
||||||
|
- [ ] Remove unused Dexie imports
|
||||||
|
- [ ] Clean up legacy database code
|
||||||
|
- [ ] Update all documentation
|
||||||
|
- [ ] Remove migration tools
|
||||||
|
|
||||||
|
### Phase 4: Fence Removal 🎯
|
||||||
|
- [ ] Remove `USE_DEXIE_DB` constant
|
||||||
|
- [ ] Remove Dexie dependencies
|
||||||
|
- [ ] Remove migration service
|
||||||
|
- [ ] Finalize SQLite-only architecture
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### 1. Data Protection
|
||||||
|
- **Encryption**: Maintain encryption standards across migration
|
||||||
|
- **Access Control**: Preserve user privacy during migration
|
||||||
|
- **Audit Trail**: Log all migration operations
|
||||||
|
|
||||||
|
### 2. Error Handling
|
||||||
|
- **Graceful Degradation**: Handle migration failures gracefully
|
||||||
|
- **User Communication**: Clear messaging about migration status
|
||||||
|
- **Recovery Options**: Provide rollback mechanisms
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
### 1. Migration Performance
|
||||||
|
- **Batch Operations**: Use transactions for bulk data transfer
|
||||||
|
- **Progress Indicators**: Show migration progress to users
|
||||||
|
- **Background Processing**: Non-blocking migration operations
|
||||||
|
|
||||||
|
### 2. Application Performance
|
||||||
|
- **Query Optimization**: Optimize SQLite queries for performance
|
||||||
|
- **Indexing Strategy**: Maintain proper database indexes
|
||||||
|
- **Memory Management**: Efficient memory usage during migration
|
||||||
|
|
||||||
|
## Documentation Requirements
|
||||||
|
|
||||||
|
### 1. Code Documentation
|
||||||
|
- **Migration Fence Comments**: Document fence boundaries in code
|
||||||
|
- **API Documentation**: Update all database API documentation
|
||||||
|
- **Migration Guides**: Comprehensive migration documentation
|
||||||
|
|
||||||
|
### 2. User Documentation
|
||||||
|
- **Migration Instructions**: Clear user migration steps
|
||||||
|
- **Troubleshooting**: Common migration issues and solutions
|
||||||
|
- **Rollback Instructions**: How to revert if needed
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
The migration fence provides a controlled boundary between legacy and new database systems, ensuring:
|
||||||
|
- **Data Integrity**: No data loss during migration
|
||||||
|
- **Application Stability**: Consistent behavior across platforms
|
||||||
|
- **Development Clarity**: Clear guidelines for code development
|
||||||
|
- **Migration Safety**: Controlled and reversible migration process
|
||||||
|
|
||||||
|
This fence will remain in place until all data is successfully migrated and verified, at which point the legacy system can be safely removed.
|
||||||
355
doc/migration-security-checklist.md
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
# Database Migration Security Audit Checklist
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document provides a comprehensive security audit checklist for the Dexie to SQLite migration in TimeSafari. The checklist ensures that data protection, privacy, and security are maintained throughout the migration process.
|
||||||
|
|
||||||
|
## Pre-Migration Security Assessment
|
||||||
|
|
||||||
|
### 1. Data Classification and Sensitivity
|
||||||
|
|
||||||
|
- [ ] **Data Inventory**
|
||||||
|
- [ ] Identify all sensitive data types (DIDs, private keys, personal information)
|
||||||
|
- [ ] Document data retention requirements
|
||||||
|
- [ ] Map data relationships and dependencies
|
||||||
|
- [ ] Assess data sensitivity levels (public, internal, confidential, restricted)
|
||||||
|
|
||||||
|
- [ ] **Encryption Assessment**
|
||||||
|
- [ ] Verify current encryption methods for sensitive data
|
||||||
|
- [ ] Document encryption keys and their management
|
||||||
|
- [ ] Assess encryption strength and compliance
|
||||||
|
- [ ] Plan encryption migration strategy
|
||||||
|
|
||||||
|
### 2. Access Control Review
|
||||||
|
|
||||||
|
- [ ] **User Access Rights**
|
||||||
|
- [ ] Audit current user permissions and roles
|
||||||
|
- [ ] Document access control mechanisms
|
||||||
|
- [ ] Verify principle of least privilege
|
||||||
|
- [ ] Plan access control migration
|
||||||
|
|
||||||
|
- [ ] **System Access**
|
||||||
|
- [ ] Review database access patterns
|
||||||
|
- [ ] Document authentication mechanisms
|
||||||
|
- [ ] Assess session management
|
||||||
|
- [ ] Plan authentication migration
|
||||||
|
|
||||||
|
### 3. Compliance Requirements
|
||||||
|
|
||||||
|
- [ ] **Regulatory Compliance**
|
||||||
|
- [ ] Identify applicable regulations (GDPR, CCPA, etc.)
|
||||||
|
- [ ] Document data processing requirements
|
||||||
|
- [ ] Assess privacy impact
|
||||||
|
- [ ] Plan compliance verification
|
||||||
|
|
||||||
|
- [ ] **Industry Standards**
|
||||||
|
- [ ] Review security standards compliance
|
||||||
|
- [ ] Document security controls
|
||||||
|
- [ ] Assess audit requirements
|
||||||
|
- [ ] Plan standards compliance
|
||||||
|
|
||||||
|
## Migration Security Controls
|
||||||
|
|
||||||
|
### 1. Data Protection During Migration
|
||||||
|
|
||||||
|
- [ ] **Encryption in Transit**
|
||||||
|
- [ ] Verify all data transfers are encrypted
|
||||||
|
- [ ] Use secure communication protocols (TLS 1.3+)
|
||||||
|
- [ ] Implement secure API endpoints
|
||||||
|
- [ ] Monitor encryption status
|
||||||
|
|
||||||
|
- [ ] **Encryption at Rest**
|
||||||
|
- [ ] Maintain encryption for stored data
|
||||||
|
- [ ] Verify encryption key management
|
||||||
|
- [ ] Test encryption/decryption processes
|
||||||
|
- [ ] Document encryption procedures
|
||||||
|
|
||||||
|
### 2. Access Control During Migration
|
||||||
|
|
||||||
|
- [ ] **Authentication**
|
||||||
|
- [ ] Maintain user authentication during migration
|
||||||
|
- [ ] Verify session management
|
||||||
|
- [ ] Implement secure token handling
|
||||||
|
- [ ] Monitor authentication events
|
||||||
|
|
||||||
|
- [ ] **Authorization**
|
||||||
|
- [ ] Preserve user permissions during migration
|
||||||
|
- [ ] Verify role-based access control
|
||||||
|
- [ ] Implement audit logging
|
||||||
|
- [ ] Monitor access patterns
|
||||||
|
|
||||||
|
### 3. Data Integrity
|
||||||
|
|
||||||
|
- [ ] **Data Validation**
|
||||||
|
- [ ] Implement input validation for all data
|
||||||
|
- [ ] Verify data format consistency
|
||||||
|
- [ ] Test data transformation processes
|
||||||
|
- [ ] Document validation rules
|
||||||
|
|
||||||
|
- [ ] **Data Verification**
|
||||||
|
- [ ] Implement checksums for data integrity
|
||||||
|
- [ ] Verify data completeness after migration
|
||||||
|
- [ ] Test data consistency checks
|
||||||
|
- [ ] Document verification procedures
|
||||||
|
|
||||||
|
## Migration Process Security
|
||||||
|
|
||||||
|
### 1. Backup Security
|
||||||
|
|
||||||
|
- [ ] **Backup Creation**
|
||||||
|
- [ ] Create encrypted backups before migration
|
||||||
|
- [ ] Verify backup integrity
|
||||||
|
- [ ] Store backups securely
|
||||||
|
- [ ] Test backup restoration
|
||||||
|
|
||||||
|
- [ ] **Backup Access**
|
||||||
|
- [ ] Limit backup access to authorized personnel
|
||||||
|
- [ ] Implement backup access logging
|
||||||
|
- [ ] Verify backup encryption
|
||||||
|
- [ ] Document backup procedures
|
||||||
|
|
||||||
|
### 2. Migration Tool Security
|
||||||
|
|
||||||
|
- [ ] **Tool Authentication**
|
||||||
|
- [ ] Implement secure authentication for migration tools
|
||||||
|
- [ ] Verify tool access controls
|
||||||
|
- [ ] Monitor tool usage
|
||||||
|
- [ ] Document tool security
|
||||||
|
|
||||||
|
- [ ] **Tool Validation**
|
||||||
|
- [ ] Verify migration tool integrity
|
||||||
|
- [ ] Test tool security features
|
||||||
|
- [ ] Validate tool outputs
|
||||||
|
- [ ] Document tool validation
|
||||||
|
|
||||||
|
### 3. Error Handling
|
||||||
|
|
||||||
|
- [ ] **Error Security**
|
||||||
|
- [ ] Implement secure error handling
|
||||||
|
- [ ] Avoid information disclosure in errors
|
||||||
|
- [ ] Log security-relevant errors
|
||||||
|
- [ ] Document error procedures
|
||||||
|
|
||||||
|
- [ ] **Recovery Security**
|
||||||
|
- [ ] Implement secure recovery procedures
|
||||||
|
- [ ] Verify recovery data protection
|
||||||
|
- [ ] Test recovery processes
|
||||||
|
- [ ] Document recovery security
|
||||||
|
|
||||||
|
## Post-Migration Security
|
||||||
|
|
||||||
|
### 1. Data Verification
|
||||||
|
|
||||||
|
- [ ] **Data Completeness**
|
||||||
|
- [ ] Verify all data was migrated successfully
|
||||||
|
- [ ] Check for data corruption
|
||||||
|
- [ ] Validate data relationships
|
||||||
|
- [ ] Document verification results
|
||||||
|
|
||||||
|
- [ ] **Data Accuracy**
|
||||||
|
- [ ] Verify data accuracy after migration
|
||||||
|
- [ ] Test data consistency
|
||||||
|
- [ ] Validate data integrity
|
||||||
|
- [ ] Document accuracy checks
|
||||||
|
|
||||||
|
### 2. Access Control Verification
|
||||||
|
|
||||||
|
- [ ] **User Access**
|
||||||
|
- [ ] Verify user access rights after migration
|
||||||
|
- [ ] Test authentication mechanisms
|
||||||
|
- [ ] Validate authorization rules
|
||||||
|
- [ ] Document access verification
|
||||||
|
|
||||||
|
- [ ] **System Access**
|
||||||
|
- [ ] Verify system access controls
|
||||||
|
- [ ] Test API security
|
||||||
|
- [ ] Validate session management
|
||||||
|
- [ ] Document system security
|
||||||
|
|
||||||
|
### 3. Security Testing
|
||||||
|
|
||||||
|
- [ ] **Penetration Testing**
|
||||||
|
- [ ] Conduct security penetration testing
|
||||||
|
- [ ] Test for common vulnerabilities
|
||||||
|
- [ ] Verify security controls
|
||||||
|
- [ ] Document test results
|
||||||
|
|
||||||
|
- [ ] **Vulnerability Assessment**
|
||||||
|
- [ ] Scan for security vulnerabilities
|
||||||
|
- [ ] Assess security posture
|
||||||
|
- [ ] Identify security gaps
|
||||||
|
- [ ] Document assessment results
|
||||||
|
|
||||||
|
## Monitoring and Logging
|
||||||
|
|
||||||
|
### 1. Security Monitoring
|
||||||
|
|
||||||
|
- [ ] **Access Monitoring**
|
||||||
|
- [ ] Monitor database access patterns
|
||||||
|
- [ ] Track user authentication events
|
||||||
|
- [ ] Monitor system access
|
||||||
|
- [ ] Document monitoring procedures
|
||||||
|
|
||||||
|
- [ ] **Data Monitoring**
|
||||||
|
- [ ] Monitor data access patterns
|
||||||
|
- [ ] Track data modification events
|
||||||
|
- [ ] Monitor data integrity
|
||||||
|
- [ ] Document data monitoring
|
||||||
|
|
||||||
|
### 2. Security Logging
|
||||||
|
|
||||||
|
- [ ] **Audit Logging**
|
||||||
|
- [ ] Implement comprehensive audit logging
|
||||||
|
- [ ] Log all security-relevant events
|
||||||
|
- [ ] Secure log storage and access
|
||||||
|
- [ ] Document logging procedures
|
||||||
|
|
||||||
|
- [ ] **Log Analysis**
|
||||||
|
- [ ] Implement log analysis tools
|
||||||
|
- [ ] Monitor for security incidents
|
||||||
|
- [ ] Analyze security trends
|
||||||
|
- [ ] Document analysis procedures
|
||||||
|
|
||||||
|
## Incident Response
|
||||||
|
|
||||||
|
### 1. Security Incident Planning
|
||||||
|
|
||||||
|
- [ ] **Incident Response Plan**
|
||||||
|
- [ ] Develop security incident response plan
|
||||||
|
- [ ] Define incident response procedures
|
||||||
|
- [ ] Train incident response team
|
||||||
|
- [ ] Document response procedures
|
||||||
|
|
||||||
|
- [ ] **Incident Detection**
|
||||||
|
- [ ] Implement incident detection mechanisms
|
||||||
|
- [ ] Monitor for security incidents
|
||||||
|
- [ ] Establish incident reporting procedures
|
||||||
|
- [ ] Document detection procedures
|
||||||
|
|
||||||
|
### 2. Recovery Procedures
|
||||||
|
|
||||||
|
- [ ] **Data Recovery**
|
||||||
|
- [ ] Develop data recovery procedures
|
||||||
|
- [ ] Test recovery processes
|
||||||
|
- [ ] Verify recovery data integrity
|
||||||
|
- [ ] Document recovery procedures
|
||||||
|
|
||||||
|
- [ ] **System Recovery**
|
||||||
|
- [ ] Develop system recovery procedures
|
||||||
|
- [ ] Test system recovery
|
||||||
|
- [ ] Verify system security after recovery
|
||||||
|
- [ ] Document recovery procedures
|
||||||
|
|
||||||
|
## Compliance Verification
|
||||||
|
|
||||||
|
### 1. Regulatory Compliance
|
||||||
|
|
||||||
|
- [ ] **Privacy Compliance**
|
||||||
|
- [ ] Verify GDPR compliance
|
||||||
|
- [ ] Check CCPA compliance
|
||||||
|
- [ ] Assess other privacy regulations
|
||||||
|
- [ ] Document compliance status
|
||||||
|
|
||||||
|
- [ ] **Security Compliance**
|
||||||
|
- [ ] Verify security standard compliance
|
||||||
|
- [ ] Check industry requirements
|
||||||
|
- [ ] Assess security certifications
|
||||||
|
- [ ] Document compliance status
|
||||||
|
|
||||||
|
### 2. Audit Requirements
|
||||||
|
|
||||||
|
- [ ] **Audit Trail**
|
||||||
|
- [ ] Maintain comprehensive audit trail
|
||||||
|
- [ ] Verify audit log integrity
|
||||||
|
- [ ] Test audit log accessibility
|
||||||
|
- [ ] Document audit procedures
|
||||||
|
|
||||||
|
- [ ] **Audit Reporting**
|
||||||
|
- [ ] Generate audit reports
|
||||||
|
- [ ] Verify report accuracy
|
||||||
|
- [ ] Distribute reports securely
|
||||||
|
- [ ] Document reporting procedures
|
||||||
|
|
||||||
|
## Documentation and Training
|
||||||
|
|
||||||
|
### 1. Security Documentation
|
||||||
|
|
||||||
|
- [ ] **Security Procedures**
|
||||||
|
- [ ] Document security procedures
|
||||||
|
- [ ] Update security policies
|
||||||
|
- [ ] Create security guidelines
|
||||||
|
- [ ] Maintain documentation
|
||||||
|
|
||||||
|
- [ ] **Security Training**
|
||||||
|
- [ ] Develop security training materials
|
||||||
|
- [ ] Train staff on security procedures
|
||||||
|
- [ ] Verify training effectiveness
|
||||||
|
- [ ] Document training procedures
|
||||||
|
|
||||||
|
### 2. Ongoing Security
|
||||||
|
|
||||||
|
- [ ] **Security Maintenance**
|
||||||
|
- [ ] Establish security maintenance procedures
|
||||||
|
- [ ] Schedule security updates
|
||||||
|
- [ ] Monitor security trends
|
||||||
|
- [ ] Document maintenance procedures
|
||||||
|
|
||||||
|
- [ ] **Security Review**
|
||||||
|
- [ ] Conduct regular security reviews
|
||||||
|
- [ ] Update security controls
|
||||||
|
- [ ] Assess security effectiveness
|
||||||
|
- [ ] Document review procedures
|
||||||
|
|
||||||
|
## Risk Assessment
|
||||||
|
|
||||||
|
### 1. Risk Identification
|
||||||
|
|
||||||
|
- [ ] **Security Risks**
|
||||||
|
- [ ] Identify potential security risks
|
||||||
|
- [ ] Assess risk likelihood and impact
|
||||||
|
- [ ] Prioritize security risks
|
||||||
|
- [ ] Document risk assessment
|
||||||
|
|
||||||
|
- [ ] **Mitigation Strategies**
|
||||||
|
- [ ] Develop risk mitigation strategies
|
||||||
|
- [ ] Implement risk controls
|
||||||
|
- [ ] Monitor risk status
|
||||||
|
- [ ] Document mitigation procedures
|
||||||
|
|
||||||
|
### 2. Risk Monitoring
|
||||||
|
|
||||||
|
- [ ] **Risk Tracking**
|
||||||
|
- [ ] Track identified risks
|
||||||
|
- [ ] Monitor risk status
|
||||||
|
- [ ] Update risk assessments
|
||||||
|
- [ ] Document risk tracking
|
||||||
|
|
||||||
|
- [ ] **Risk Reporting**
|
||||||
|
- [ ] Generate risk reports
|
||||||
|
- [ ] Distribute risk information
|
||||||
|
- [ ] Update risk documentation
|
||||||
|
- [ ] Document reporting procedures
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
This security audit checklist ensures that the database migration maintains the highest standards of data protection, privacy, and security. Regular review and updates of this checklist are essential to maintain security throughout the migration process and beyond.
|
||||||
|
|
||||||
|
### Security Checklist Summary
|
||||||
|
|
||||||
|
- [ ] **Pre-Migration Assessment**: Complete
|
||||||
|
- [ ] **Migration Controls**: Complete
|
||||||
|
- [ ] **Process Security**: Complete
|
||||||
|
- [ ] **Post-Migration Verification**: Complete
|
||||||
|
- [ ] **Monitoring and Logging**: Complete
|
||||||
|
- [ ] **Incident Response**: Complete
|
||||||
|
- [ ] **Compliance Verification**: Complete
|
||||||
|
- [ ] **Documentation and Training**: Complete
|
||||||
|
- [ ] **Risk Assessment**: Complete
|
||||||
|
|
||||||
|
**Overall Security Status**: [ ] Secure [ ] Needs Attention [ ] Critical Issues
|
||||||
|
|
||||||
|
**Next Review Date**: _______________
|
||||||
|
|
||||||
|
**Reviewed By**: _______________
|
||||||
|
|
||||||
|
**Approved By**: _______________
|
||||||
@@ -4,610 +4,223 @@
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
**Current Status**: The migration is in **Phase 2** with a well-defined migration fence in place. Core settings and account data have been migrated, with contact migration in progress. **ActiveDid migration has been implemented** to ensure user identity continuity.
|
||||||
|
|
||||||
## Migration Goals
|
## Migration Goals
|
||||||
|
|
||||||
1. **Data Integrity**
|
1. **Data Integrity**
|
||||||
- Preserve all existing data
|
- Preserve all existing data
|
||||||
- Maintain data relationships
|
- Maintain data relationships
|
||||||
- Ensure data consistency
|
- Ensure data consistency
|
||||||
|
- **Preserve user's active identity**
|
||||||
|
|
||||||
2. **Performance**
|
2. **Performance**
|
||||||
- Improve query performance
|
- Improve query performance
|
||||||
- Reduce storage overhead
|
- Reduce storage overhead
|
||||||
- Optimize for platform-specific features
|
- Optimize for platform-specific capabilities
|
||||||
|
|
||||||
3. **Security**
|
3. **User Experience**
|
||||||
- Maintain or improve encryption
|
- Seamless transition with no data loss
|
||||||
- Preserve access controls
|
- Maintain user's active identity and preferences
|
||||||
- Enhance data protection
|
- Preserve application state
|
||||||
|
|
||||||
4. **User Experience**
|
## Migration Architecture
|
||||||
- Zero data loss
|
|
||||||
- Minimal downtime
|
|
||||||
- Automatic migration where possible
|
|
||||||
|
|
||||||
## Prerequisites
|
### Migration Fence
|
||||||
|
The migration fence is defined by the `USE_DEXIE_DB` constant in `src/constants/app.ts`:
|
||||||
|
- `USE_DEXIE_DB = false` (default): Uses SQLite database
|
||||||
|
- `USE_DEXIE_DB = true`: Uses Dexie database (for migration purposes)
|
||||||
|
|
||||||
1. **Backup Requirements**
|
### Migration Order
|
||||||
```typescript
|
The migration follows a specific order to maintain data integrity:
|
||||||
interface MigrationBackup {
|
|
||||||
timestamp: number;
|
|
||||||
accounts: Account[];
|
|
||||||
settings: Setting[];
|
|
||||||
contacts: Contact[];
|
|
||||||
metadata: {
|
|
||||||
version: string;
|
|
||||||
platform: string;
|
|
||||||
dexieVersion: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Dependencies**
|
1. **Accounts** (foundational - contains DIDs)
|
||||||
```json
|
2. **Settings** (references accountDid, activeDid)
|
||||||
{
|
3. **ActiveDid** (depends on accounts and settings) ⭐ **NEW**
|
||||||
"@jlongster/sql.js": "^1.8.0",
|
4. **Contacts** (independent, but migrated after accounts for consistency)
|
||||||
"absurd-sql": "^1.8.0"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Storage Requirements**
|
## ActiveDid Migration ⭐ **NEW FEATURE**
|
||||||
- Sufficient IndexedDB quota
|
|
||||||
- Available disk space for SQLite
|
|
||||||
- Backup storage space
|
|
||||||
|
|
||||||
4. **Platform Support**
|
### Problem Solved
|
||||||
- Web: Modern browser with IndexedDB support
|
Previously, the `activeDid` setting was not migrated from Dexie to SQLite, causing users to lose their active identity after migration.
|
||||||
- iOS: iOS 13+ with SQLite support
|
|
||||||
- Android: Android 5+ with SQLite support
|
### Solution Implemented
|
||||||
- Electron: Latest version with SQLite support
|
The migration now includes a dedicated step for migrating the `activeDid`:
|
||||||
|
|
||||||
|
1. **Detection**: Identifies the `activeDid` from Dexie master settings
|
||||||
|
2. **Validation**: Verifies the `activeDid` exists in SQLite accounts
|
||||||
|
3. **Migration**: Updates SQLite master settings with the `activeDid`
|
||||||
|
4. **Error Handling**: Graceful handling of missing accounts
|
||||||
|
|
||||||
|
### Implementation Details
|
||||||
|
|
||||||
|
#### New Function: `migrateActiveDid()`
|
||||||
|
```typescript
|
||||||
|
export async function migrateActiveDid(): Promise<MigrationResult> {
|
||||||
|
// 1. Get Dexie settings to find the activeDid
|
||||||
|
const dexieSettings = await getDexieSettings();
|
||||||
|
const masterSettings = dexieSettings.find(setting => !setting.accountDid);
|
||||||
|
|
||||||
|
// 2. Verify the activeDid exists in SQLite accounts
|
||||||
|
const accountExists = await platformService.dbQuery(
|
||||||
|
"SELECT did FROM accounts WHERE did = ?",
|
||||||
|
[dexieActiveDid],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Update SQLite master settings
|
||||||
|
await updateDefaultSettings({ activeDid: dexieActiveDid });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Enhanced `migrateSettings()` Function
|
||||||
|
The settings migration now includes activeDid handling:
|
||||||
|
- Extracts `activeDid` from Dexie master settings
|
||||||
|
- Validates account existence in SQLite
|
||||||
|
- Updates SQLite master settings with the `activeDid`
|
||||||
|
|
||||||
|
#### Updated `migrateAll()` Function
|
||||||
|
The complete migration now includes a dedicated step for activeDid:
|
||||||
|
```typescript
|
||||||
|
// Step 3: Migrate ActiveDid (depends on accounts and settings)
|
||||||
|
logger.info("[MigrationService] Step 3: Migrating activeDid...");
|
||||||
|
const activeDidResult = await migrateActiveDid();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Benefits
|
||||||
|
- ✅ **User Identity Preservation**: Users maintain their active identity
|
||||||
|
- ✅ **Seamless Experience**: No need to manually select identity after migration
|
||||||
|
- ✅ **Data Consistency**: Ensures all identity-related settings are preserved
|
||||||
|
- ✅ **Error Resilience**: Graceful handling of edge cases
|
||||||
|
|
||||||
## Migration Process
|
## Migration Process
|
||||||
|
|
||||||
### 1. Preparation
|
### Phase 1: Preparation ✅
|
||||||
|
- [x] Enable Dexie database access
|
||||||
|
- [x] Implement data comparison tools
|
||||||
|
- [x] Create migration service structure
|
||||||
|
|
||||||
|
### Phase 2: Core Migration ✅
|
||||||
|
- [x] Account migration with `importFromMnemonic`
|
||||||
|
- [x] Settings migration (excluding activeDid)
|
||||||
|
- [x] **ActiveDid migration** ⭐ **COMPLETED**
|
||||||
|
- [x] Contact migration framework
|
||||||
|
|
||||||
|
### Phase 3: Validation and Cleanup 🔄
|
||||||
|
- [ ] Comprehensive data validation
|
||||||
|
- [ ] Performance testing
|
||||||
|
- [ ] User acceptance testing
|
||||||
|
- [ ] Dexie removal
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Manual Migration
|
||||||
```typescript
|
```typescript
|
||||||
// src/services/storage/migration/MigrationService.ts
|
import { migrateAll, migrateActiveDid } from '../services/indexedDBMigrationService';
|
||||||
import initSqlJs from '@jlongster/sql.js';
|
|
||||||
import { SQLiteFS } from 'absurd-sql';
|
|
||||||
import IndexedDBBackend from 'absurd-sql/dist/indexeddb-backend';
|
|
||||||
|
|
||||||
export class MigrationService {
|
// Complete migration
|
||||||
private static instance: MigrationService;
|
const result = await migrateAll();
|
||||||
private backup: MigrationBackup | null = null;
|
|
||||||
private sql: any = null;
|
|
||||||
private db: any = null;
|
|
||||||
|
|
||||||
async prepare(): Promise<void> {
|
// Or migrate just the activeDid
|
||||||
try {
|
const activeDidResult = await migrateActiveDid();
|
||||||
// 1. Check prerequisites
|
|
||||||
await this.checkPrerequisites();
|
|
||||||
|
|
||||||
// 2. Create backup
|
|
||||||
this.backup = await this.createBackup();
|
|
||||||
|
|
||||||
// 3. Verify backup integrity
|
|
||||||
await this.verifyBackup();
|
|
||||||
|
|
||||||
// 4. Initialize absurd-sql
|
|
||||||
await this.initializeAbsurdSql();
|
|
||||||
} catch (error) {
|
|
||||||
throw new StorageError(
|
|
||||||
'Migration preparation failed',
|
|
||||||
StorageErrorCodes.MIGRATION_FAILED,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async initializeAbsurdSql(): Promise<void> {
|
|
||||||
// Initialize SQL.js
|
|
||||||
this.sql = await initSqlJs({
|
|
||||||
locateFile: (file: string) => {
|
|
||||||
return new URL(`/node_modules/@jlongster/sql.js/dist/${file}`, import.meta.url).href;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Setup SQLiteFS with IndexedDB backend
|
|
||||||
const sqlFS = new SQLiteFS(this.sql.FS, new IndexedDBBackend());
|
|
||||||
this.sql.register_for_idb(sqlFS);
|
|
||||||
|
|
||||||
// Create and mount filesystem
|
|
||||||
this.sql.FS.mkdir('/sql');
|
|
||||||
this.sql.FS.mount(sqlFS, {}, '/sql');
|
|
||||||
|
|
||||||
// Open database
|
|
||||||
const path = '/sql/db.sqlite';
|
|
||||||
if (typeof SharedArrayBuffer === 'undefined') {
|
|
||||||
let stream = this.sql.FS.open(path, 'a+');
|
|
||||||
await stream.node.contents.readIfFallback();
|
|
||||||
this.sql.FS.close(stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.db = new this.sql.Database(path, { filename: true });
|
|
||||||
if (!this.db) {
|
|
||||||
throw new StorageError(
|
|
||||||
'Database initialization failed',
|
|
||||||
StorageErrorCodes.INITIALIZATION_FAILED
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure database
|
|
||||||
await this.db.exec(`PRAGMA journal_mode=MEMORY;`);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async checkPrerequisites(): Promise<void> {
|
|
||||||
// Check IndexedDB availability
|
|
||||||
if (!window.indexedDB) {
|
|
||||||
throw new StorageError(
|
|
||||||
'IndexedDB not available',
|
|
||||||
StorageErrorCodes.INITIALIZATION_FAILED
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check storage quota
|
|
||||||
const quota = await navigator.storage.estimate();
|
|
||||||
if (quota.quota && quota.usage && quota.usage > quota.quota * 0.9) {
|
|
||||||
throw new StorageError(
|
|
||||||
'Insufficient storage space',
|
|
||||||
StorageErrorCodes.STORAGE_FULL
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check platform support
|
|
||||||
const capabilities = await PlatformDetection.getCapabilities();
|
|
||||||
if (!capabilities.hasFileSystem) {
|
|
||||||
throw new StorageError(
|
|
||||||
'Platform does not support required features',
|
|
||||||
StorageErrorCodes.INITIALIZATION_FAILED
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async createBackup(): Promise<MigrationBackup> {
|
|
||||||
const dexieDB = new Dexie('TimeSafariDB');
|
|
||||||
|
|
||||||
return {
|
|
||||||
timestamp: Date.now(),
|
|
||||||
accounts: await dexieDB.accounts.toArray(),
|
|
||||||
settings: await dexieDB.settings.toArray(),
|
|
||||||
contacts: await dexieDB.contacts.toArray(),
|
|
||||||
metadata: {
|
|
||||||
version: '1.0.0',
|
|
||||||
platform: await PlatformDetection.getPlatform(),
|
|
||||||
dexieVersion: Dexie.version
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Data Migration
|
### Migration Verification
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// src/services/storage/migration/DataMigration.ts
|
import { compareDatabases } from '../services/indexedDBMigrationService';
|
||||||
export class DataMigration {
|
|
||||||
async migrate(backup: MigrationBackup): Promise<void> {
|
|
||||||
try {
|
|
||||||
// 1. Create new database schema
|
|
||||||
await this.createSchema();
|
|
||||||
|
|
||||||
// 2. Migrate accounts
|
|
||||||
await this.migrateAccounts(backup.accounts);
|
|
||||||
|
|
||||||
// 3. Migrate settings
|
|
||||||
await this.migrateSettings(backup.settings);
|
|
||||||
|
|
||||||
// 4. Migrate contacts
|
|
||||||
await this.migrateContacts(backup.contacts);
|
|
||||||
|
|
||||||
// 5. Verify migration
|
|
||||||
await this.verifyMigration(backup);
|
|
||||||
} catch (error) {
|
|
||||||
// 6. Handle failure
|
|
||||||
await this.handleMigrationFailure(error, backup);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async migrateAccounts(accounts: Account[]): Promise<void> {
|
const comparison = await compareDatabases();
|
||||||
// Use transaction for atomicity
|
console.log('Migration differences:', comparison.differences);
|
||||||
await this.db.exec('BEGIN TRANSACTION;');
|
|
||||||
try {
|
|
||||||
for (const account of accounts) {
|
|
||||||
await this.db.run(`
|
|
||||||
INSERT INTO accounts (did, public_key_hex, created_at, updated_at)
|
|
||||||
VALUES (?, ?, ?, ?)
|
|
||||||
`, [
|
|
||||||
account.did,
|
|
||||||
account.publicKeyHex,
|
|
||||||
account.createdAt,
|
|
||||||
account.updatedAt
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
await this.db.exec('COMMIT;');
|
|
||||||
} catch (error) {
|
|
||||||
await this.db.exec('ROLLBACK;');
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async verifyMigration(backup: MigrationBackup): Promise<void> {
|
|
||||||
// Verify account count
|
|
||||||
const result = await this.db.exec('SELECT COUNT(*) as count FROM accounts');
|
|
||||||
const accountCount = result[0].values[0][0];
|
|
||||||
|
|
||||||
if (accountCount !== backup.accounts.length) {
|
|
||||||
throw new StorageError(
|
|
||||||
'Account count mismatch',
|
|
||||||
StorageErrorCodes.VERIFICATION_FAILED
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify data integrity
|
|
||||||
await this.verifyDataIntegrity(backup);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Rollback Strategy
|
## Error Handling
|
||||||
|
|
||||||
|
### ActiveDid Migration Errors
|
||||||
|
- **Missing Account**: If the `activeDid` from Dexie doesn't exist in SQLite accounts
|
||||||
|
- **Database Errors**: Connection or query failures
|
||||||
|
- **Settings Update Failures**: Issues updating SQLite master settings
|
||||||
|
|
||||||
|
### Recovery Strategies
|
||||||
|
1. **Automatic Recovery**: Migration continues even if activeDid migration fails
|
||||||
|
2. **Manual Recovery**: Users can manually select their identity after migration
|
||||||
|
3. **Fallback**: System creates new identity if none exists
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### Data Protection
|
||||||
|
- All sensitive data (mnemonics, private keys) are encrypted
|
||||||
|
- Migration preserves encryption standards
|
||||||
|
- No plaintext data exposure during migration
|
||||||
|
|
||||||
|
### Identity Verification
|
||||||
|
- ActiveDid migration validates account existence
|
||||||
|
- Prevents setting non-existent identities as active
|
||||||
|
- Maintains cryptographic integrity
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Migration Testing
|
||||||
|
```bash
|
||||||
|
# Enable Dexie for testing
|
||||||
|
# Set USE_DEXIE_DB = true in constants/app.ts
|
||||||
|
|
||||||
|
# Run migration
|
||||||
|
npm run migrate
|
||||||
|
|
||||||
|
# Verify results
|
||||||
|
npm run test:migration
|
||||||
|
```
|
||||||
|
|
||||||
|
### ActiveDid Testing
|
||||||
```typescript
|
```typescript
|
||||||
// src/services/storage/migration/RollbackService.ts
|
// Test activeDid migration specifically
|
||||||
export class RollbackService {
|
const result = await migrateActiveDid();
|
||||||
async rollback(backup: MigrationBackup): Promise<void> {
|
expect(result.success).toBe(true);
|
||||||
try {
|
expect(result.warnings).toContain('Successfully migrated activeDid');
|
||||||
// 1. Stop all database operations
|
|
||||||
await this.stopDatabaseOperations();
|
|
||||||
|
|
||||||
// 2. Restore from backup
|
|
||||||
await this.restoreFromBackup(backup);
|
|
||||||
|
|
||||||
// 3. Verify restoration
|
|
||||||
await this.verifyRestoration(backup);
|
|
||||||
|
|
||||||
// 4. Clean up absurd-sql
|
|
||||||
await this.cleanupAbsurdSql();
|
|
||||||
} catch (error) {
|
|
||||||
throw new StorageError(
|
|
||||||
'Rollback failed',
|
|
||||||
StorageErrorCodes.ROLLBACK_FAILED,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async restoreFromBackup(backup: MigrationBackup): Promise<void> {
|
|
||||||
const dexieDB = new Dexie('TimeSafariDB');
|
|
||||||
|
|
||||||
// Restore accounts
|
|
||||||
await dexieDB.accounts.bulkPut(backup.accounts);
|
|
||||||
|
|
||||||
// Restore settings
|
|
||||||
await dexieDB.settings.bulkPut(backup.settings);
|
|
||||||
|
|
||||||
// Restore contacts
|
|
||||||
await dexieDB.contacts.bulkPut(backup.contacts);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Migration UI
|
## Troubleshooting
|
||||||
|
|
||||||
```vue
|
### Common Issues
|
||||||
<!-- src/components/MigrationProgress.vue -->
|
|
||||||
<template>
|
|
||||||
<div class="migration-progress">
|
|
||||||
<h2>Database Migration</h2>
|
|
||||||
|
|
||||||
<div class="progress-container">
|
|
||||||
<div class="progress-bar" :style="{ width: `${progress}%` }" />
|
|
||||||
<div class="progress-text">{{ progress }}%</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="status-message">{{ statusMessage }}</div>
|
|
||||||
|
|
||||||
<div v-if="error" class="error-message">
|
|
||||||
{{ error }}
|
|
||||||
<button @click="retryMigration">Retry</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
1. **ActiveDid Not Found**
|
||||||
import { ref, onMounted } from 'vue';
|
- Ensure accounts were migrated before activeDid migration
|
||||||
import { MigrationService } from '@/services/storage/migration/MigrationService';
|
- Check that the Dexie activeDid exists in SQLite accounts
|
||||||
|
|
||||||
const progress = ref(0);
|
2. **Migration Failures**
|
||||||
const statusMessage = ref('Preparing migration...');
|
- Verify Dexie database is accessible
|
||||||
const error = ref<string | null>(null);
|
- Check SQLite database permissions
|
||||||
|
- Review migration logs for specific errors
|
||||||
|
|
||||||
const migrationService = MigrationService.getInstance();
|
3. **Data Inconsistencies**
|
||||||
|
- Use `compareDatabases()` to identify differences
|
||||||
|
- Re-run migration if necessary
|
||||||
|
- Check for duplicate or conflicting records
|
||||||
|
|
||||||
async function startMigration() {
|
### Debugging
|
||||||
try {
|
```typescript
|
||||||
// 1. Preparation
|
// Enable detailed logging
|
||||||
statusMessage.value = 'Creating backup...';
|
logger.setLevel('debug');
|
||||||
await migrationService.prepare();
|
|
||||||
progress.value = 20;
|
|
||||||
|
|
||||||
// 2. Data migration
|
|
||||||
statusMessage.value = 'Migrating data...';
|
|
||||||
await migrationService.migrate();
|
|
||||||
progress.value = 80;
|
|
||||||
|
|
||||||
// 3. Verification
|
|
||||||
statusMessage.value = 'Verifying migration...';
|
|
||||||
await migrationService.verify();
|
|
||||||
progress.value = 100;
|
|
||||||
|
|
||||||
statusMessage.value = 'Migration completed successfully!';
|
|
||||||
} catch (err) {
|
|
||||||
error.value = err instanceof Error ? err.message : 'Migration failed';
|
|
||||||
statusMessage.value = 'Migration failed';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function retryMigration() {
|
// Check migration status
|
||||||
error.value = null;
|
const comparison = await compareDatabases();
|
||||||
progress.value = 0;
|
console.log('Settings differences:', comparison.differences.settings);
|
||||||
await startMigration();
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
startMigration();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.migration-progress {
|
|
||||||
padding: 2rem;
|
|
||||||
max-width: 600px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-container {
|
|
||||||
position: relative;
|
|
||||||
height: 20px;
|
|
||||||
background: #eee;
|
|
||||||
border-radius: 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
margin: 1rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-bar {
|
|
||||||
position: absolute;
|
|
||||||
height: 100%;
|
|
||||||
background: #4CAF50;
|
|
||||||
transition: width 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-text {
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 20px;
|
|
||||||
color: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-message {
|
|
||||||
text-align: center;
|
|
||||||
margin: 1rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message {
|
|
||||||
color: #f44336;
|
|
||||||
text-align: center;
|
|
||||||
margin: 1rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
margin-top: 1rem;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
background: #2196F3;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background: #1976D2;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Testing Strategy
|
## Future Enhancements
|
||||||
|
|
||||||
1. **Unit Tests**
|
### Planned Improvements
|
||||||
```typescript
|
1. **Batch Processing**: Optimize for large datasets
|
||||||
// src/services/storage/migration/__tests__/MigrationService.spec.ts
|
2. **Incremental Migration**: Support partial migrations
|
||||||
describe('MigrationService', () => {
|
3. **Rollback Capability**: Ability to revert migration
|
||||||
it('should initialize absurd-sql correctly', async () => {
|
4. **Progress Tracking**: Real-time migration progress
|
||||||
const service = MigrationService.getInstance();
|
|
||||||
await service.initializeAbsurdSql();
|
|
||||||
|
|
||||||
expect(service.isInitialized()).toBe(true);
|
|
||||||
expect(service.getDatabase()).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create valid backup', async () => {
|
### Performance Optimizations
|
||||||
const service = MigrationService.getInstance();
|
1. **Parallel Processing**: Migrate independent data concurrently
|
||||||
const backup = await service.createBackup();
|
2. **Memory Management**: Optimize for large datasets
|
||||||
|
3. **Transaction Batching**: Reduce database round trips
|
||||||
expect(backup).toBeDefined();
|
|
||||||
expect(backup.accounts).toBeInstanceOf(Array);
|
|
||||||
expect(backup.settings).toBeInstanceOf(Array);
|
|
||||||
expect(backup.contacts).toBeInstanceOf(Array);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should migrate data correctly', async () => {
|
## Conclusion
|
||||||
const service = MigrationService.getInstance();
|
|
||||||
const backup = await service.createBackup();
|
|
||||||
|
|
||||||
await service.migrate(backup);
|
|
||||||
|
|
||||||
// Verify migration
|
|
||||||
const accounts = await service.getMigratedAccounts();
|
|
||||||
expect(accounts).toHaveLength(backup.accounts.length);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle rollback correctly', async () => {
|
The Dexie to SQLite migration provides a robust, secure, and user-friendly transition path. The addition of activeDid migration ensures that users maintain their identity continuity throughout the migration process, significantly improving the user experience.
|
||||||
const service = MigrationService.getInstance();
|
|
||||||
const backup = await service.createBackup();
|
|
||||||
|
|
||||||
// Simulate failed migration
|
|
||||||
await service.migrate(backup);
|
|
||||||
await service.simulateFailure();
|
|
||||||
|
|
||||||
// Perform rollback
|
|
||||||
await service.rollback(backup);
|
|
||||||
|
|
||||||
// Verify rollback
|
|
||||||
const accounts = await service.getOriginalAccounts();
|
|
||||||
expect(accounts).toHaveLength(backup.accounts.length);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Integration Tests**
|
The migration fence architecture allows for controlled, reversible migration while maintaining application stability and data integrity.
|
||||||
```typescript
|
|
||||||
// src/services/storage/migration/__tests__/integration/Migration.spec.ts
|
|
||||||
describe('Migration Integration', () => {
|
|
||||||
it('should handle concurrent access during migration', async () => {
|
|
||||||
const service = MigrationService.getInstance();
|
|
||||||
|
|
||||||
// Start migration
|
|
||||||
const migrationPromise = service.migrate();
|
|
||||||
|
|
||||||
// Simulate concurrent access
|
|
||||||
const accessPromises = Array(5).fill(null).map(() =>
|
|
||||||
service.getAccount('did:test:123')
|
|
||||||
);
|
|
||||||
|
|
||||||
// Wait for all operations
|
|
||||||
const [migrationResult, ...accessResults] = await Promise.allSettled([
|
|
||||||
migrationPromise,
|
|
||||||
...accessPromises
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Verify results
|
|
||||||
expect(migrationResult.status).toBe('fulfilled');
|
|
||||||
expect(accessResults.some(r => r.status === 'rejected')).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should maintain data integrity during platform transition', async () => {
|
|
||||||
const service = MigrationService.getInstance();
|
|
||||||
|
|
||||||
// Simulate platform change
|
|
||||||
await service.simulatePlatformChange();
|
|
||||||
|
|
||||||
// Verify data
|
|
||||||
const accounts = await service.getAllAccounts();
|
|
||||||
const settings = await service.getAllSettings();
|
|
||||||
const contacts = await service.getAllContacts();
|
|
||||||
|
|
||||||
expect(accounts).toBeDefined();
|
|
||||||
expect(settings).toBeDefined();
|
|
||||||
expect(contacts).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Success Criteria
|
|
||||||
|
|
||||||
1. **Data Integrity**
|
|
||||||
- [ ] All accounts migrated successfully
|
|
||||||
- [ ] All settings preserved
|
|
||||||
- [ ] All contacts transferred
|
|
||||||
- [ ] No data corruption
|
|
||||||
|
|
||||||
2. **Performance**
|
|
||||||
- [ ] Migration completes within acceptable time
|
|
||||||
- [ ] No significant performance degradation
|
|
||||||
- [ ] Efficient storage usage
|
|
||||||
- [ ] Smooth user experience
|
|
||||||
|
|
||||||
3. **Security**
|
|
||||||
- [ ] Encrypted data remains secure
|
|
||||||
- [ ] Access controls maintained
|
|
||||||
- [ ] No sensitive data exposure
|
|
||||||
- [ ] Secure backup process
|
|
||||||
|
|
||||||
4. **User Experience**
|
|
||||||
- [ ] Clear migration progress
|
|
||||||
- [ ] Informative error messages
|
|
||||||
- [ ] Automatic recovery from failures
|
|
||||||
- [ ] No data loss
|
|
||||||
|
|
||||||
## Rollback Plan
|
|
||||||
|
|
||||||
1. **Automatic Rollback**
|
|
||||||
- Triggered by migration failure
|
|
||||||
- Restores from verified backup
|
|
||||||
- Maintains data consistency
|
|
||||||
- Logs rollback reason
|
|
||||||
|
|
||||||
2. **Manual Rollback**
|
|
||||||
- Available through settings
|
|
||||||
- Requires user confirmation
|
|
||||||
- Preserves backup data
|
|
||||||
- Provides rollback status
|
|
||||||
|
|
||||||
3. **Emergency Recovery**
|
|
||||||
- Manual backup restoration
|
|
||||||
- Database repair tools
|
|
||||||
- Data recovery procedures
|
|
||||||
- Support contact information
|
|
||||||
|
|
||||||
## Post-Migration
|
|
||||||
|
|
||||||
1. **Verification**
|
|
||||||
- Data integrity checks
|
|
||||||
- Performance monitoring
|
|
||||||
- Error rate tracking
|
|
||||||
- User feedback collection
|
|
||||||
|
|
||||||
2. **Cleanup**
|
|
||||||
- Remove old database
|
|
||||||
- Clear migration artifacts
|
|
||||||
- Update application state
|
|
||||||
- Archive backup data
|
|
||||||
|
|
||||||
3. **Monitoring**
|
|
||||||
- Track migration success rate
|
|
||||||
- Monitor performance metrics
|
|
||||||
- Collect error reports
|
|
||||||
- Gather user feedback
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
For assistance with migration:
|
|
||||||
1. Check the troubleshooting guide
|
|
||||||
2. Review error logs
|
|
||||||
3. Contact support team
|
|
||||||
4. Submit issue report
|
|
||||||
|
|
||||||
## Timeline
|
|
||||||
|
|
||||||
1. **Preparation Phase** (1 week)
|
|
||||||
- Backup system implementation
|
|
||||||
- Migration service development
|
|
||||||
- Testing framework setup
|
|
||||||
|
|
||||||
2. **Testing Phase** (2 weeks)
|
|
||||||
- Unit testing
|
|
||||||
- Integration testing
|
|
||||||
- Performance testing
|
|
||||||
- Security testing
|
|
||||||
|
|
||||||
3. **Deployment Phase** (1 week)
|
|
||||||
- Staged rollout
|
|
||||||
- Monitoring
|
|
||||||
- Support preparation
|
|
||||||
- Documentation updates
|
|
||||||
|
|
||||||
4. **Post-Deployment** (2 weeks)
|
|
||||||
- Monitoring
|
|
||||||
- Bug fixes
|
|
||||||
- Performance optimization
|
|
||||||
- User feedback collection
|
|
||||||
@@ -3,45 +3,46 @@
|
|||||||
## Core Services
|
## Core Services
|
||||||
|
|
||||||
### 1. Storage Service Layer
|
### 1. Storage Service Layer
|
||||||
- [ ] Create base `StorageService` interface
|
- [x] Create base `PlatformService` interface
|
||||||
- [ ] Define common methods for all platforms
|
- [x] Define common methods for all platforms
|
||||||
- [ ] Add platform-specific method signatures
|
- [x] Add platform-specific method signatures
|
||||||
- [ ] Include error handling types
|
- [x] Include error handling types
|
||||||
- [ ] Add migration support methods
|
- [x] Add migration support methods
|
||||||
|
|
||||||
- [ ] Implement platform-specific services
|
- [x] Implement platform-specific services
|
||||||
- [ ] `WebSQLiteService` (absurd-sql)
|
- [x] `AbsurdSqlDatabaseService` (web)
|
||||||
- [ ] Database initialization
|
- [x] Database initialization
|
||||||
- [ ] VFS setup with IndexedDB backend
|
- [x] VFS setup with IndexedDB backend
|
||||||
- [ ] Connection management
|
- [x] Connection management
|
||||||
- [ ] Query builder
|
- [x] Operation queuing
|
||||||
- [ ] `NativeSQLiteService` (iOS/Android)
|
- [ ] `NativeSQLiteService` (iOS/Android) (planned)
|
||||||
- [ ] SQLCipher integration
|
- [ ] SQLCipher integration
|
||||||
- [ ] Native bridge setup
|
- [ ] Native bridge setup
|
||||||
- [ ] File system access
|
- [ ] File system access
|
||||||
- [ ] `ElectronSQLiteService`
|
- [ ] `ElectronSQLiteService` (planned)
|
||||||
- [ ] Node SQLite integration
|
- [ ] Node SQLite integration
|
||||||
- [ ] IPC communication
|
- [ ] IPC communication
|
||||||
- [ ] File system access
|
- [ ] File system access
|
||||||
|
|
||||||
### 2. Migration Services
|
### 2. Migration Services
|
||||||
- [ ] Implement `MigrationService`
|
- [x] Implement basic migration support
|
||||||
- [ ] Backup creation
|
- [x] Dual-storage pattern (SQLite + Dexie)
|
||||||
- [ ] Data verification
|
- [x] Basic data verification
|
||||||
- [ ] Rollback procedures
|
- [ ] Rollback procedures (planned)
|
||||||
- [ ] Progress tracking
|
- [ ] Progress tracking (planned)
|
||||||
- [ ] Create `MigrationUI` components
|
- [ ] Create `MigrationUI` components (planned)
|
||||||
- [ ] Progress indicators
|
- [ ] Progress indicators
|
||||||
- [ ] Error handling
|
- [ ] Error handling
|
||||||
- [ ] User notifications
|
- [ ] User notifications
|
||||||
- [ ] Manual triggers
|
- [ ] Manual triggers
|
||||||
|
|
||||||
### 3. Security Layer
|
### 3. Security Layer
|
||||||
- [ ] Implement `EncryptionService`
|
- [x] Basic data integrity
|
||||||
|
- [ ] Implement `EncryptionService` (planned)
|
||||||
- [ ] Key management
|
- [ ] Key management
|
||||||
- [ ] Encryption/decryption
|
- [ ] Encryption/decryption
|
||||||
- [ ] Secure storage
|
- [ ] Secure storage
|
||||||
- [ ] Add `BiometricService`
|
- [ ] Add `BiometricService` (planned)
|
||||||
- [ ] Platform detection
|
- [ ] Platform detection
|
||||||
- [ ] Authentication flow
|
- [ ] Authentication flow
|
||||||
- [ ] Fallback mechanisms
|
- [ ] Fallback mechanisms
|
||||||
@@ -49,18 +50,19 @@
|
|||||||
## Platform-Specific Implementation
|
## Platform-Specific Implementation
|
||||||
|
|
||||||
### Web Platform
|
### Web Platform
|
||||||
- [ ] Setup absurd-sql
|
- [x] Setup absurd-sql
|
||||||
- [ ] Install dependencies
|
- [x] Install dependencies
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"@jlongster/sql.js": "^1.8.0",
|
"@jlongster/sql.js": "^1.8.0",
|
||||||
"absurd-sql": "^1.8.0"
|
"absurd-sql": "^1.8.0"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
- [ ] Configure VFS with IndexedDB backend
|
- [x] Configure VFS with IndexedDB backend
|
||||||
- [ ] Setup worker threads
|
- [x] Setup worker threads
|
||||||
- [ ] Implement connection pooling
|
- [x] Implement operation queuing
|
||||||
- [ ] Configure database pragmas
|
- [x] Configure database pragmas
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
PRAGMA journal_mode=MEMORY;
|
PRAGMA journal_mode=MEMORY;
|
||||||
PRAGMA synchronous=NORMAL;
|
PRAGMA synchronous=NORMAL;
|
||||||
@@ -68,19 +70,19 @@
|
|||||||
PRAGMA busy_timeout=5000;
|
PRAGMA busy_timeout=5000;
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] Update build configuration
|
- [x] Update build configuration
|
||||||
- [ ] Modify `vite.config.ts`
|
- [x] Modify `vite.config.ts`
|
||||||
- [ ] Add worker configuration
|
- [x] Add worker configuration
|
||||||
- [ ] Update chunk splitting
|
- [x] Update chunk splitting
|
||||||
- [ ] Configure asset handling
|
- [x] Configure asset handling
|
||||||
|
|
||||||
- [ ] Implement IndexedDB fallback
|
- [x] Implement IndexedDB backend
|
||||||
- [ ] Create fallback service
|
- [x] Create database service
|
||||||
- [ ] Add data synchronization
|
- [x] Add operation queuing
|
||||||
- [ ] Handle quota exceeded
|
- [x] Handle initialization
|
||||||
- [ ] Implement atomic operations
|
- [x] Implement atomic operations
|
||||||
|
|
||||||
### iOS Platform
|
### iOS Platform (Planned)
|
||||||
- [ ] Setup SQLCipher
|
- [ ] Setup SQLCipher
|
||||||
- [ ] Install pod dependencies
|
- [ ] Install pod dependencies
|
||||||
- [ ] Configure encryption
|
- [ ] Configure encryption
|
||||||
@@ -93,7 +95,7 @@
|
|||||||
- [ ] Configure backup
|
- [ ] Configure backup
|
||||||
- [ ] Setup app groups
|
- [ ] Setup app groups
|
||||||
|
|
||||||
### Android Platform
|
### Android Platform (Planned)
|
||||||
- [ ] Setup SQLCipher
|
- [ ] Setup SQLCipher
|
||||||
- [ ] Add Gradle dependencies
|
- [ ] Add Gradle dependencies
|
||||||
- [ ] Configure encryption
|
- [ ] Configure encryption
|
||||||
@@ -106,7 +108,7 @@
|
|||||||
- [ ] Configure backup
|
- [ ] Configure backup
|
||||||
- [ ] Setup file provider
|
- [ ] Setup file provider
|
||||||
|
|
||||||
### Electron Platform
|
### Electron Platform (Planned)
|
||||||
- [ ] Setup Node SQLite
|
- [ ] Setup Node SQLite
|
||||||
- [ ] Install dependencies
|
- [ ] Install dependencies
|
||||||
- [ ] Configure IPC
|
- [ ] Configure IPC
|
||||||
@@ -122,7 +124,8 @@
|
|||||||
## Data Models and Types
|
## Data Models and Types
|
||||||
|
|
||||||
### 1. Database Schema
|
### 1. Database Schema
|
||||||
- [ ] Define tables
|
- [x] Define tables
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
-- Accounts table
|
-- Accounts table
|
||||||
CREATE TABLE accounts (
|
CREATE TABLE accounts (
|
||||||
@@ -155,13 +158,14 @@
|
|||||||
CREATE INDEX idx_settings_updated_at ON settings(updated_at);
|
CREATE INDEX idx_settings_updated_at ON settings(updated_at);
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] Create indexes
|
- [x] Create indexes
|
||||||
- [ ] Define constraints
|
- [x] Define constraints
|
||||||
- [ ] Add triggers
|
- [ ] Add triggers (planned)
|
||||||
- [ ] Setup migrations
|
- [ ] Setup migrations (planned)
|
||||||
|
|
||||||
### 2. Type Definitions
|
### 2. Type Definitions
|
||||||
- [ ] Create interfaces
|
|
||||||
|
- [x] Create interfaces
|
||||||
```typescript
|
```typescript
|
||||||
interface Account {
|
interface Account {
|
||||||
did: string;
|
did: string;
|
||||||
@@ -185,28 +189,28 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] Add validation
|
- [x] Add validation
|
||||||
- [ ] Create DTOs
|
- [x] Create DTOs
|
||||||
- [ ] Define enums
|
- [x] Define enums
|
||||||
- [ ] Add type guards
|
- [x] Add type guards
|
||||||
|
|
||||||
## UI Components
|
## UI Components
|
||||||
|
|
||||||
### 1. Migration UI
|
### 1. Migration UI (Planned)
|
||||||
- [ ] Create components
|
- [ ] Create components
|
||||||
- [ ] `MigrationProgress.vue`
|
- [ ] `MigrationProgress.vue`
|
||||||
- [ ] `MigrationError.vue`
|
- [ ] `MigrationError.vue`
|
||||||
- [ ] `MigrationSettings.vue`
|
- [ ] `MigrationSettings.vue`
|
||||||
- [ ] `MigrationStatus.vue`
|
- [ ] `MigrationStatus.vue`
|
||||||
|
|
||||||
### 2. Settings UI
|
### 2. Settings UI (Planned)
|
||||||
- [ ] Update components
|
- [ ] Update components
|
||||||
- [ ] Add storage settings
|
- [ ] Add storage settings
|
||||||
- [ ] Add migration controls
|
- [ ] Add migration controls
|
||||||
- [ ] Add backup options
|
- [ ] Add backup options
|
||||||
- [ ] Add security settings
|
- [ ] Add security settings
|
||||||
|
|
||||||
### 3. Error Handling UI
|
### 3. Error Handling UI (Planned)
|
||||||
- [ ] Create components
|
- [ ] Create components
|
||||||
- [ ] `StorageError.vue`
|
- [ ] `StorageError.vue`
|
||||||
- [ ] `QuotaExceeded.vue`
|
- [ ] `QuotaExceeded.vue`
|
||||||
@@ -216,20 +220,20 @@
|
|||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
### 1. Unit Tests
|
### 1. Unit Tests
|
||||||
- [ ] Test services
|
- [x] Basic service tests
|
||||||
- [ ] Storage service tests
|
- [x] Platform service tests
|
||||||
- [ ] Migration service tests
|
- [x] Database operation tests
|
||||||
- [ ] Security service tests
|
- [ ] Security service tests (planned)
|
||||||
- [ ] Platform detection tests
|
- [ ] Platform detection tests (planned)
|
||||||
|
|
||||||
### 2. Integration Tests
|
### 2. Integration Tests (Planned)
|
||||||
- [ ] Test migrations
|
- [ ] Test migrations
|
||||||
- [ ] Web platform tests
|
- [ ] Web platform tests
|
||||||
- [ ] iOS platform tests
|
- [ ] iOS platform tests
|
||||||
- [ ] Android platform tests
|
- [ ] Android platform tests
|
||||||
- [ ] Electron platform tests
|
- [ ] Electron platform tests
|
||||||
|
|
||||||
### 3. E2E Tests
|
### 3. E2E Tests (Planned)
|
||||||
- [ ] Test workflows
|
- [ ] Test workflows
|
||||||
- [ ] Account management
|
- [ ] Account management
|
||||||
- [ ] Settings management
|
- [ ] Settings management
|
||||||
@@ -239,12 +243,12 @@
|
|||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
### 1. Technical Documentation
|
### 1. Technical Documentation
|
||||||
- [ ] Update architecture docs
|
- [x] Update architecture docs
|
||||||
- [ ] Add API documentation
|
- [x] Add API documentation
|
||||||
- [ ] Create migration guides
|
- [ ] Create migration guides (planned)
|
||||||
- [ ] Document security measures
|
- [ ] Document security measures (planned)
|
||||||
|
|
||||||
### 2. User Documentation
|
### 2. User Documentation (Planned)
|
||||||
- [ ] Update user guides
|
- [ ] Update user guides
|
||||||
- [ ] Add troubleshooting guides
|
- [ ] Add troubleshooting guides
|
||||||
- [ ] Create FAQ
|
- [ ] Create FAQ
|
||||||
@@ -253,18 +257,18 @@
|
|||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
### 1. Build Process
|
### 1. Build Process
|
||||||
- [ ] Update build scripts
|
- [x] Update build scripts
|
||||||
- [ ] Add platform-specific builds
|
- [x] Add platform-specific builds
|
||||||
- [ ] Configure CI/CD
|
- [ ] Configure CI/CD (planned)
|
||||||
- [ ] Setup automated testing
|
- [ ] Setup automated testing (planned)
|
||||||
|
|
||||||
### 2. Release Process
|
### 2. Release Process (Planned)
|
||||||
- [ ] Create release checklist
|
- [ ] Create release checklist
|
||||||
- [ ] Add version management
|
- [ ] Add version management
|
||||||
- [ ] Setup rollback procedures
|
- [ ] Setup rollback procedures
|
||||||
- [ ] Configure monitoring
|
- [ ] Configure monitoring
|
||||||
|
|
||||||
## Monitoring and Analytics
|
## Monitoring and Analytics (Planned)
|
||||||
|
|
||||||
### 1. Error Tracking
|
### 1. Error Tracking
|
||||||
- [ ] Setup error logging
|
- [ ] Setup error logging
|
||||||
@@ -278,7 +282,7 @@
|
|||||||
- [ ] Monitor performance
|
- [ ] Monitor performance
|
||||||
- [ ] Collect user feedback
|
- [ ] Collect user feedback
|
||||||
|
|
||||||
## Security Audit
|
## Security Audit (Planned)
|
||||||
|
|
||||||
### 1. Code Review
|
### 1. Code Review
|
||||||
- [ ] Review encryption
|
- [ ] Review encryption
|
||||||
@@ -295,29 +299,31 @@
|
|||||||
## Success Criteria
|
## Success Criteria
|
||||||
|
|
||||||
### 1. Performance
|
### 1. Performance
|
||||||
- [ ] Query response time < 100ms
|
- [x] Query response time < 100ms
|
||||||
- [ ] Migration time < 5s per 1000 records
|
- [x] Operation queuing for thread safety
|
||||||
- [ ] Storage overhead < 10%
|
- [x] Proper initialization handling
|
||||||
- [ ] Memory usage < 50MB
|
- [ ] Migration time < 5s per 1000 records (planned)
|
||||||
- [ ] Atomic operations complete successfully
|
- [ ] Storage overhead < 10% (planned)
|
||||||
- [ ] Transaction performance meets requirements
|
- [ ] Memory usage < 50MB (planned)
|
||||||
|
|
||||||
### 2. Reliability
|
### 2. Reliability
|
||||||
- [ ] 99.9% uptime
|
- [x] Basic data integrity
|
||||||
- [ ] Zero data loss
|
- [x] Operation queuing
|
||||||
- [ ] Automatic recovery
|
- [ ] Automatic recovery (planned)
|
||||||
- [ ] Backup verification
|
- [ ] Backup verification (planned)
|
||||||
- [ ] Transaction atomicity
|
- [ ] Transaction atomicity (planned)
|
||||||
- [ ] Data consistency
|
- [ ] Data consistency (planned)
|
||||||
|
|
||||||
### 3. Security
|
### 3. Security
|
||||||
- [ ] AES-256 encryption
|
- [x] Basic data integrity
|
||||||
- [ ] Secure key storage
|
- [ ] AES-256 encryption (planned)
|
||||||
- [ ] Access control
|
- [ ] Secure key storage (planned)
|
||||||
- [ ] Audit logging
|
- [ ] Access control (planned)
|
||||||
|
- [ ] Audit logging (planned)
|
||||||
|
|
||||||
### 4. User Experience
|
### 4. User Experience
|
||||||
- [ ] Smooth migration
|
- [x] Basic database operations
|
||||||
- [ ] Clear error messages
|
- [ ] Smooth migration (planned)
|
||||||
- [ ] Progress indicators
|
- [ ] Clear error messages (planned)
|
||||||
- [ ] Recovery options
|
- [ ] Progress indicators (planned)
|
||||||
|
- [ ] Recovery options (planned)
|
||||||
13
ios/.gitignore
vendored
@@ -11,3 +11,16 @@ capacitor-cordova-ios-plugins
|
|||||||
# Generated Config files
|
# Generated Config files
|
||||||
App/App/capacitor.config.json
|
App/App/capacitor.config.json
|
||||||
App/App/config.xml
|
App/App/config.xml
|
||||||
|
|
||||||
|
# User-specific Xcode files
|
||||||
|
App/App.xcodeproj/xcuserdata/*.xcuserdatad/
|
||||||
|
App/App.xcodeproj/*.xcuserstate
|
||||||
|
|
||||||
|
fastlane/report.xml
|
||||||
|
fastlane/Preview.html
|
||||||
|
fastlane/screenshots
|
||||||
|
fastlane/test_output
|
||||||
|
|
||||||
|
# Generated Icons from capacitor-assets (also Contents.json which is confusing; see BUILDING.md)
|
||||||
|
App/App/Assets.xcassets/AppIcon.appiconset
|
||||||
|
App/App/Assets.xcassets/Splash.imageset
|
||||||
|
|||||||
@@ -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,12 @@
|
|||||||
};
|
};
|
||||||
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 = 35;
|
||||||
|
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||||
ENABLE_APP_SANDBOX = NO;
|
ENABLE_APP_SANDBOX = NO;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
@@ -388,7 +413,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.4.7;
|
MARKETING_VERSION = 1.0.2;
|
||||||
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)";
|
||||||
@@ -401,11 +426,12 @@
|
|||||||
};
|
};
|
||||||
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 = 35;
|
||||||
|
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||||
ENABLE_APP_SANDBOX = NO;
|
ENABLE_APP_SANDBOX = NO;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
@@ -414,7 +440,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.4.7;
|
MARKETING_VERSION = 1.0.2;
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 116 KiB |
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"images": [
|
|
||||||
{
|
|
||||||
"idiom": "universal",
|
|
||||||
"size": "1024x1024",
|
|
||||||
"filename": "AppIcon-512@2x.png",
|
|
||||||
"platform": "ios"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info": {
|
|
||||||
"author": "xcode",
|
|
||||||
"version": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"filename" : "splash-2732x2732-2.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"filename" : "splash-2732x2732-1.png",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"filename" : "splash-2732x2732.png",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"version" : 1,
|
|
||||||
"author" : "xcode"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 40 KiB |
@@ -49,5 +49,16 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>app.timesafari</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>timesafari</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
4607
package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "timesafari",
|
"name": "timesafari",
|
||||||
"version": "0.4.6",
|
"version": "1.0.3-beta",
|
||||||
"description": "Time Safari Application",
|
"description": "Time Safari Application",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Time Safari Team"
|
"name": "Time Safari Team"
|
||||||
@@ -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",
|
||||||
@@ -166,11 +167,11 @@
|
|||||||
"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": {
|
||||||
"appId": "app.timesafari",
|
"appId": "app.timesafari.app",
|
||||||
"productName": "TimeSafari",
|
"productName": "TimeSafari",
|
||||||
"directories": {
|
"directories": {
|
||||||
"output": "dist-electron-packages"
|
"output": "dist-electron-packages"
|
||||||
@@ -181,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!');
|
|
||||||
@@ -51,7 +51,7 @@ const { existsSync } = require('fs');
|
|||||||
*/
|
*/
|
||||||
function checkCommand(command, errorMessage) {
|
function checkCommand(command, errorMessage) {
|
||||||
try {
|
try {
|
||||||
execSync(command + ' --version', { stdio: 'ignore' });
|
execSync(command, { stdio: 'ignore' });
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`❌ ${errorMessage}`);
|
console.error(`❌ ${errorMessage}`);
|
||||||
@@ -164,10 +164,10 @@ function main() {
|
|||||||
|
|
||||||
// Check required command line tools
|
// Check required command line tools
|
||||||
// These are essential for building and testing the application
|
// These are essential for building and testing the application
|
||||||
success &= checkCommand('node', 'Node.js is required');
|
success &= checkCommand('node --version', 'Node.js is required');
|
||||||
success &= checkCommand('npm', 'npm is required');
|
success &= checkCommand('npm --version', 'npm is required');
|
||||||
success &= checkCommand('gradle', 'Gradle is required for Android builds');
|
success &= checkCommand('gradle --version', 'Gradle is required for Android builds');
|
||||||
success &= checkCommand('xcodebuild', 'Xcode is required for iOS builds');
|
success &= checkCommand('xcodebuild --help', 'Xcode is required for iOS builds');
|
||||||
|
|
||||||
// Check platform-specific development environments
|
// Check platform-specific development environments
|
||||||
success &= checkAndroidSetup();
|
success &= checkAndroidSetup();
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ const executeDeeplink = async (url, description, log) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Stop the app before executing the deep link
|
// Stop the app before executing the deep link
|
||||||
execSync('adb shell am force-stop app.timesafari');
|
execSync('adb shell am force-stop app.timesafari.app');
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1s
|
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1s
|
||||||
|
|
||||||
execSync(`adb shell am start -W -a android.intent.action.VIEW -d "${url}" -c android.intent.category.BROWSABLE`);
|
execSync(`adb shell am start -W -a android.intent.action.VIEW -d "${url}" -c android.intent.category.BROWSABLE`);
|
||||||
|
|||||||
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>
|
||||||
|
|||||||
75
src/assets/icons.json
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
{
|
||||||
|
"warning": {
|
||||||
|
"fillRule": "evenodd",
|
||||||
|
"d": "M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z",
|
||||||
|
"clipRule": "evenodd"
|
||||||
|
},
|
||||||
|
"spinner": {
|
||||||
|
"d": "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
},
|
||||||
|
"chart": {
|
||||||
|
"strokeLinecap": "round",
|
||||||
|
"strokeLinejoin": "round",
|
||||||
|
"strokeWidth": "2",
|
||||||
|
"d": "M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
|
||||||
|
},
|
||||||
|
"plus": {
|
||||||
|
"strokeLinecap": "round",
|
||||||
|
"strokeLinejoin": "round",
|
||||||
|
"strokeWidth": "2",
|
||||||
|
"d": "M12 4v16m8-8H4"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"strokeLinecap": "round",
|
||||||
|
"strokeLinejoin": "round",
|
||||||
|
"strokeWidth": "2",
|
||||||
|
"d": "M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
|
||||||
|
},
|
||||||
|
"settingsDot": {
|
||||||
|
"strokeLinecap": "round",
|
||||||
|
"strokeLinejoin": "round",
|
||||||
|
"strokeWidth": "2",
|
||||||
|
"d": "M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||||
|
},
|
||||||
|
"lock": {
|
||||||
|
"strokeLinecap": "round",
|
||||||
|
"strokeLinejoin": "round",
|
||||||
|
"strokeWidth": "2",
|
||||||
|
"d": "M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
||||||
|
},
|
||||||
|
"download": {
|
||||||
|
"strokeLinecap": "round",
|
||||||
|
"strokeLinejoin": "round",
|
||||||
|
"strokeWidth": "2",
|
||||||
|
"d": "M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||||
|
},
|
||||||
|
"check": {
|
||||||
|
"strokeLinecap": "round",
|
||||||
|
"strokeLinejoin": "round",
|
||||||
|
"strokeWidth": "2",
|
||||||
|
"d": "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
},
|
||||||
|
"edit": {
|
||||||
|
"strokeLinecap": "round",
|
||||||
|
"strokeLinejoin": "round",
|
||||||
|
"strokeWidth": "2",
|
||||||
|
"d": "M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
||||||
|
},
|
||||||
|
"trash": {
|
||||||
|
"strokeLinecap": "round",
|
||||||
|
"strokeLinejoin": "round",
|
||||||
|
"strokeWidth": "2",
|
||||||
|
"d": "M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||||||
|
},
|
||||||
|
"plusCircle": {
|
||||||
|
"strokeLinecap": "round",
|
||||||
|
"strokeLinejoin": "round",
|
||||||
|
"strokeWidth": "2",
|
||||||
|
"d": "M12 6v6m0 0v6m0-6h6m-6 0H6"
|
||||||
|
},
|
||||||
|
"info": {
|
||||||
|
"fillRule": "evenodd",
|
||||||
|
"d": "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z",
|
||||||
|
"clipRule": "evenodd"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,22 +14,34 @@
|
|||||||
class="flex items-center justify-between gap-2 text-lg bg-slate-200 border border-slate-300 border-b-0 rounded-t-md px-3 sm:px-4 py-1 sm:py-2"
|
class="flex items-center justify-between gap-2 text-lg bg-slate-200 border border-slate-300 border-b-0 rounded-t-md px-3 sm:px-4 py-1 sm:py-2"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div v-if="record.issuerDid">
|
<router-link
|
||||||
|
v-if="record.issuerDid && !isHiddenDid(record.issuerDid)"
|
||||||
|
:to="{
|
||||||
|
path: '/did/' + encodeURIComponent(record.issuerDid),
|
||||||
|
}"
|
||||||
|
title="More details about this person"
|
||||||
|
>
|
||||||
<EntityIcon
|
<EntityIcon
|
||||||
:entity-id="record.issuerDid"
|
:entity-id="record.issuerDid"
|
||||||
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
|
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
</router-link>
|
||||||
<div v-else>
|
<font-awesome
|
||||||
<font-awesome
|
v-else-if="isHiddenDid(record.issuerDid)"
|
||||||
icon="person-circle-question"
|
icon="eye-slash"
|
||||||
class="text-slate-300 text-[2rem]"
|
class="text-slate-400 !size-[2rem] cursor-pointer"
|
||||||
/>
|
@click="notifyHiddenPerson"
|
||||||
</div>
|
/>
|
||||||
|
<font-awesome
|
||||||
|
v-else
|
||||||
|
icon="person-circle-question"
|
||||||
|
class="text-slate-400 !size-[2rem] cursor-pointer"
|
||||||
|
@click="notifyUnknownPerson"
|
||||||
|
/>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-semibold">
|
<h3 v-if="record.issuer.known" class="font-semibold leading-tight">
|
||||||
{{ record.issuer.known ? 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">
|
||||||
{{ friendlyDate }}
|
{{ friendlyDate }}
|
||||||
@@ -37,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>
|
||||||
@@ -46,7 +62,7 @@
|
|||||||
<!-- Record Image -->
|
<!-- Record Image -->
|
||||||
<div
|
<div
|
||||||
v-if="record.image"
|
v-if="record.image"
|
||||||
class="bg-cover mb-6 -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
|
||||||
@@ -62,29 +78,59 @@
|
|||||||
</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-5"
|
class="relative flex justify-between gap-4 max-w-[40rem] mx-auto mt-4"
|
||||||
>
|
>
|
||||||
<!-- Source -->
|
<!-- Source -->
|
||||||
<div
|
<div
|
||||||
class="w-[8rem] sm:w-[12rem] text-center bg-white border border-slate-200 rounded p-2 sm:p-3"
|
class="w-[7rem] sm:w-[12rem] text-center bg-white border border-slate-200 rounded p-2 sm:p-3"
|
||||||
>
|
>
|
||||||
<div class="relative w-fit mx-auto">
|
<div class="relative w-fit mx-auto">
|
||||||
<div>
|
<div>
|
||||||
<!-- Project Icon -->
|
<!-- Project Icon -->
|
||||||
<div v-if="record.providerPlanName">
|
<div v-if="record.providerPlanName">
|
||||||
<ProjectIcon
|
<router-link
|
||||||
:entity-id="record.providerPlanName"
|
:to="{
|
||||||
:icon-size="48"
|
path:
|
||||||
class="rounded size-[3rem] sm:size-[4rem] *:w-full *:h-full"
|
'/project/' +
|
||||||
/>
|
encodeURIComponent(record.providerPlanHandleId || ''),
|
||||||
|
}"
|
||||||
|
title="View project details"
|
||||||
|
>
|
||||||
|
<ProjectIcon
|
||||||
|
:entity-id="record.providerPlanHandleId || ''"
|
||||||
|
:icon-size="48"
|
||||||
|
class="rounded size-[3rem] sm:size-[4rem] *:w-full *:h-full"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<!-- Identicon for DIDs -->
|
<!-- Identicon for DIDs -->
|
||||||
<div v-else-if="record.agentDid">
|
<div v-else-if="record.agentDid">
|
||||||
<EntityIcon
|
<router-link
|
||||||
:entity-id="record.agentDid"
|
v-if="!isHiddenDid(record.agentDid)"
|
||||||
:profile-image-url="record.issuer.profileImageUrl"
|
:to="{
|
||||||
class="rounded-full bg-slate-100 overflow-hidden !size-[3rem] sm:!size-[4rem]"
|
path: '/did/' + encodeURIComponent(record.agentDid),
|
||||||
|
}"
|
||||||
|
title="More details about this person"
|
||||||
|
>
|
||||||
|
<EntityIcon
|
||||||
|
:entity-id="record.agentDid"
|
||||||
|
:profile-image-url="record.issuer.profileImageUrl"
|
||||||
|
class="rounded-full bg-slate-100 overflow-hidden !size-[3rem] sm:!size-[4rem]"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
|
<font-awesome
|
||||||
|
v-else
|
||||||
|
icon="eye-slash"
|
||||||
|
class="text-slate-300 !size-[3rem] sm:!size-[4rem]"
|
||||||
|
@click="notifyHiddenPerson"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- Unknown Person -->
|
<!-- Unknown Person -->
|
||||||
@@ -92,6 +138,7 @@
|
|||||||
<font-awesome
|
<font-awesome
|
||||||
icon="person-circle-question"
|
icon="person-circle-question"
|
||||||
class="text-slate-300 text-[3rem] sm:text-[4rem]"
|
class="text-slate-300 text-[3rem] sm:text-[4rem]"
|
||||||
|
@click="notifyUnknownPerson"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -110,9 +157,11 @@
|
|||||||
|
|
||||||
<!-- Arrow -->
|
<!-- Arrow -->
|
||||||
<div
|
<div
|
||||||
class="absolute inset-x-[8rem] 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-[15px]">
|
<div
|
||||||
|
class="text-sm text-center leading-none font-semibold pe-2 sm:pe-4"
|
||||||
|
>
|
||||||
{{ fetchAmount }}
|
{{ fetchAmount }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -129,24 +178,47 @@
|
|||||||
|
|
||||||
<!-- Destination -->
|
<!-- Destination -->
|
||||||
<div
|
<div
|
||||||
class="w-[8rem] sm:w-[12rem] text-center bg-white border border-slate-200 rounded p-2 sm:p-3"
|
class="w-[7rem] sm:w-[12rem] text-center bg-white border border-slate-200 rounded p-2 sm:p-3"
|
||||||
>
|
>
|
||||||
<div class="relative w-fit mx-auto">
|
<div class="relative w-fit mx-auto">
|
||||||
<div>
|
<div>
|
||||||
<!-- Project Icon -->
|
<!-- Project Icon -->
|
||||||
<div v-if="record.recipientProjectName">
|
<div v-if="record.recipientProjectName">
|
||||||
<ProjectIcon
|
<router-link
|
||||||
:entity-id="record.recipientProjectName"
|
:to="{
|
||||||
:icon-size="48"
|
path:
|
||||||
class="rounded size-[3rem] sm:size-[4rem] *:w-full *:h-full"
|
'/project/' +
|
||||||
/>
|
encodeURIComponent(record.fulfillsPlanHandleId || ''),
|
||||||
|
}"
|
||||||
|
title="View project details"
|
||||||
|
>
|
||||||
|
<ProjectIcon
|
||||||
|
:entity-id="record.fulfillsPlanHandleId || ''"
|
||||||
|
:icon-size="48"
|
||||||
|
class="rounded size-[3rem] sm:size-[4rem] *:w-full *:h-full"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<!-- Identicon for DIDs -->
|
<!-- Identicon for DIDs -->
|
||||||
<div v-else-if="record.recipientDid">
|
<div v-else-if="record.recipientDid">
|
||||||
<EntityIcon
|
<router-link
|
||||||
:entity-id="record.recipientDid"
|
v-if="!isHiddenDid(record.recipientDid)"
|
||||||
:profile-image-url="record.receiver.profileImageUrl"
|
:to="{
|
||||||
class="rounded-full bg-slate-100 overflow-hidden !size-[3rem] sm:!size-[4rem]"
|
path: '/did/' + encodeURIComponent(record.recipientDid),
|
||||||
|
}"
|
||||||
|
title="More details about this person"
|
||||||
|
>
|
||||||
|
<EntityIcon
|
||||||
|
:entity-id="record.recipientDid"
|
||||||
|
:profile-image-url="record.receiver.profileImageUrl"
|
||||||
|
class="rounded-full bg-slate-100 overflow-hidden !size-[3rem] sm:!size-[4rem]"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
|
<font-awesome
|
||||||
|
v-else
|
||||||
|
icon="eye-slash"
|
||||||
|
class="text-slate-300 !size-[3rem] sm:!size-[4rem]"
|
||||||
|
@click="notifyHiddenPerson"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- Unknown Person -->
|
<!-- Unknown Person -->
|
||||||
@@ -154,6 +226,7 @@
|
|||||||
<font-awesome
|
<font-awesome
|
||||||
icon="person-circle-question"
|
icon="person-circle-question"
|
||||||
class="text-slate-300 text-[3rem] sm:text-[4rem]"
|
class="text-slate-300 text-[3rem] sm:text-[4rem]"
|
||||||
|
@click="notifyUnknownPerson"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -170,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>
|
||||||
@@ -186,8 +252,9 @@ import { Component, Prop, Vue, Emit } from "vue-facing-decorator";
|
|||||||
import { GiveRecordWithContactInfo } from "../types";
|
import { GiveRecordWithContactInfo } from "../types";
|
||||||
import EntityIcon from "./EntityIcon.vue";
|
import EntityIcon from "./EntityIcon.vue";
|
||||||
import { isGiveClaimType, notifyWhyCannotConfirm } from "../libs/util";
|
import { isGiveClaimType, notifyWhyCannotConfirm } from "../libs/util";
|
||||||
import { containsHiddenDid } from "../libs/endorserServer";
|
import { containsHiddenDid, isHiddenDid } from "../libs/endorserServer";
|
||||||
import ProjectIcon from "./ProjectIcon.vue";
|
import ProjectIcon from "./ProjectIcon.vue";
|
||||||
|
import { NotificationIface } from "../constants/app";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
@@ -202,6 +269,33 @@ export default class ActivityListItem extends Vue {
|
|||||||
@Prop() activeDid!: string;
|
@Prop() activeDid!: string;
|
||||||
@Prop() confirmerIdList?: string[];
|
@Prop() confirmerIdList?: string[];
|
||||||
|
|
||||||
|
isHiddenDid = isHiddenDid;
|
||||||
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||||
|
|
||||||
|
notifyHiddenPerson() {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "warning",
|
||||||
|
title: "Person Outside Your Network",
|
||||||
|
text: "This person is not visible to you.",
|
||||||
|
},
|
||||||
|
3000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyUnknownPerson() {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "warning",
|
||||||
|
title: "Unidentified Person",
|
||||||
|
text: "Nobody specific was recognized.",
|
||||||
|
},
|
||||||
|
3000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Emit()
|
@Emit()
|
||||||
cacheImage(image: string) {
|
cacheImage(image: string) {
|
||||||
return image;
|
return image;
|
||||||
@@ -222,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,6 +99,9 @@ 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";
|
||||||
|
|
||||||
@@ -122,7 +125,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) {
|
||||||
@@ -136,17 +142,29 @@ export default class FeedFilters extends Vue {
|
|||||||
async toggleHasVisibleDid() {
|
async toggleHasVisibleDid() {
|
||||||
this.settingChanged = true;
|
this.settingChanged = true;
|
||||||
this.hasVisibleDid = !this.hasVisibleDid;
|
this.hasVisibleDid = !this.hasVisibleDid;
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
await databaseUtil.updateDefaultSettings({
|
||||||
filterFeedByVisible: this.hasVisibleDid,
|
filterFeedByVisible: this.hasVisibleDid,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
filterFeedByVisible: this.hasVisibleDid,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async toggleNearby() {
|
async toggleNearby() {
|
||||||
this.settingChanged = true;
|
this.settingChanged = true;
|
||||||
this.isNearby = !this.isNearby;
|
this.isNearby = !this.isNearby;
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
await databaseUtil.updateDefaultSettings({
|
||||||
filterFeedByNearby: this.isNearby,
|
filterFeedByNearby: this.isNearby,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
filterFeedByNearby: this.isNearby,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearAll() {
|
async clearAll() {
|
||||||
@@ -154,11 +172,18 @@ export default class FeedFilters extends Vue {
|
|||||||
this.settingChanged = true;
|
this.settingChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
await databaseUtil.updateDefaultSettings({
|
||||||
filterFeedByNearby: false,
|
filterFeedByNearby: false,
|
||||||
filterFeedByVisible: false,
|
filterFeedByVisible: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
filterFeedByNearby: false,
|
||||||
|
filterFeedByVisible: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.hasVisibleDid = false;
|
this.hasVisibleDid = false;
|
||||||
this.isNearby = false;
|
this.isNearby = false;
|
||||||
}
|
}
|
||||||
@@ -168,11 +193,18 @@ export default class FeedFilters extends Vue {
|
|||||||
this.settingChanged = true;
|
this.settingChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
await databaseUtil.updateDefaultSettings({
|
||||||
filterFeedByNearby: true,
|
filterFeedByNearby: true,
|
||||||
filterFeedByVisible: true,
|
filterFeedByVisible: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
filterFeedByNearby: true,
|
||||||
|
filterFeedByVisible: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.hasVisibleDid = true;
|
this.hasVisibleDid = true;
|
||||||
this.isNearby = true;
|
this.isNearby = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,6 +227,7 @@ export default class GivenPrompts extends Vue {
|
|||||||
|
|
||||||
let someContactDbIndex = Math.floor(Math.random() * this.numContacts);
|
let someContactDbIndex = Math.floor(Math.random() * this.numContacts);
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
// as long as the index has an entry, loop
|
// as long as the index has an entry, loop
|
||||||
while (
|
while (
|
||||||
this.shownContactDbIndices[someContactDbIndex] != null &&
|
this.shownContactDbIndices[someContactDbIndex] != null &&
|
||||||
@@ -229,10 +240,21 @@ 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) {
|
||||||
|
const mappedContacts = databaseUtil.mapQueryResultToValues(result);
|
||||||
|
this.currentContact = mappedContacts[0] as unknown as Contact;
|
||||||
|
}
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
await db.open();
|
||||||
|
this.currentContact = await db.contacts
|
||||||
|
.offset(someContactDbIndex)
|
||||||
|
.first();
|
||||||
|
}
|
||||||
this.shownContactDbIndices[someContactDbIndex] = true;
|
this.shownContactDbIndices[someContactDbIndex] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,16 +48,15 @@
|
|||||||
<span>
|
<span>
|
||||||
{{ didInfo(visDid) }}
|
{{ didInfo(visDid) }}
|
||||||
<span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)">
|
<span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)">
|
||||||
<a
|
<router-link
|
||||||
:href="`/did/${visDid}`"
|
:to="{ path: '/did/' + encodeURIComponent(visDid) }"
|
||||||
target="_blank"
|
|
||||||
class="text-blue-500"
|
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"
|
||||||
/>
|
/>
|
||||||
</a>
|
</router-link>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -78,7 +77,7 @@
|
|||||||
If you'd like an introduction,
|
If you'd like an introduction,
|
||||||
<a
|
<a
|
||||||
class="text-blue-500"
|
class="text-blue-500"
|
||||||
@click="copyToClipboard('A link to this page', windowLocation)"
|
@click="copyToClipboard('A link to this page', deepLinkUrl)"
|
||||||
>click here to copy this page, paste it into a message, and ask if
|
>click here to copy this page, paste it into a message, and ask if
|
||||||
they'll tell you more about the {{ roleName }}.</a
|
they'll tell you more about the {{ roleName }}.</a
|
||||||
>
|
>
|
||||||
@@ -105,7 +104,7 @@ import * as R from "ramda";
|
|||||||
import { useClipboard } from "@vueuse/core";
|
import { useClipboard } from "@vueuse/core";
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from "../db/tables/contacts";
|
||||||
import * as serverUtil from "../libs/endorserServer";
|
import * as serverUtil from "../libs/endorserServer";
|
||||||
import { NotificationIface } from "../constants/app";
|
import { APP_SERVER, NotificationIface } from "../constants/app";
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class HiddenDidDialog extends Vue {
|
export default class HiddenDidDialog extends Vue {
|
||||||
@@ -118,7 +117,8 @@ export default class HiddenDidDialog extends Vue {
|
|||||||
activeDid = "";
|
activeDid = "";
|
||||||
allMyDids: Array<string> = [];
|
allMyDids: Array<string> = [];
|
||||||
canShare = false;
|
canShare = false;
|
||||||
windowLocation = window.location.href;
|
deepLinkPathSuffix = "";
|
||||||
|
deepLinkUrl = window.location.href; // this is changed to a deep link in the setup
|
||||||
|
|
||||||
R = R;
|
R = R;
|
||||||
serverUtil = serverUtil;
|
serverUtil = serverUtil;
|
||||||
@@ -130,17 +130,21 @@ export default class HiddenDidDialog extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
open(
|
open(
|
||||||
|
deepLinkPathSuffix: string,
|
||||||
roleName: string,
|
roleName: string,
|
||||||
visibleToDids: string[],
|
visibleToDids: string[],
|
||||||
allContacts: Array<Contact>,
|
allContacts: Array<Contact>,
|
||||||
activeDid: string,
|
activeDid: string,
|
||||||
allMyDids: Array<string>,
|
allMyDids: Array<string>,
|
||||||
) {
|
) {
|
||||||
|
this.deepLinkPathSuffix = deepLinkPathSuffix;
|
||||||
this.roleName = roleName;
|
this.roleName = roleName;
|
||||||
this.visibleToDids = visibleToDids;
|
this.visibleToDids = visibleToDids;
|
||||||
this.allContacts = allContacts;
|
this.allContacts = allContacts;
|
||||||
this.activeDid = activeDid;
|
this.activeDid = activeDid;
|
||||||
this.allMyDids = allMyDids;
|
this.allMyDids = allMyDids;
|
||||||
|
|
||||||
|
this.deepLinkUrl = APP_SERVER + "/deep-link/" + this.deepLinkPathSuffix;
|
||||||
this.isOpen = true;
|
this.isOpen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,11 +178,11 @@ export default class HiddenDidDialog extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onClickShareClaim() {
|
onClickShareClaim() {
|
||||||
this.copyToClipboard("A link to this page", this.windowLocation);
|
this.copyToClipboard("A link to this page", this.deepLinkUrl);
|
||||||
window.navigator.share({
|
window.navigator.share({
|
||||||
title: "Help Connect Me",
|
title: "Help Connect Me",
|
||||||
text: "I'm trying to find the people who recorded this. Can you help me?",
|
text: "I'm trying to find the people who recorded this. Can you help me?",
|
||||||
url: this.windowLocation,
|
url: this.deepLinkUrl,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
90
src/components/IconRenderer.vue
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
v-if="iconData"
|
||||||
|
:class="svgClass"
|
||||||
|
:fill="fill"
|
||||||
|
:stroke="stroke"
|
||||||
|
:viewBox="viewBox"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path v-for="(path, index) in iconData.paths" :key="index" v-bind="path" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Prop, Vue } from "vue-facing-decorator";
|
||||||
|
import icons from "../assets/icons.json";
|
||||||
|
import { logger } from "../utils/logger";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icon path interface
|
||||||
|
*/
|
||||||
|
interface IconPath {
|
||||||
|
d: string;
|
||||||
|
fillRule?: string;
|
||||||
|
clipRule?: string;
|
||||||
|
strokeLinecap?: string;
|
||||||
|
strokeLinejoin?: string;
|
||||||
|
strokeWidth?: string | number;
|
||||||
|
fill?: string;
|
||||||
|
stroke?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icon data interface
|
||||||
|
*/
|
||||||
|
interface IconData {
|
||||||
|
paths: IconPath[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icons JSON structure
|
||||||
|
*/
|
||||||
|
interface IconsJson {
|
||||||
|
[key: string]: IconPath | IconData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icon Renderer Component
|
||||||
|
*
|
||||||
|
* This component loads SVG icon definitions from a JSON file and renders them
|
||||||
|
* as SVG elements. It provides a clean way to use icons without cluttering
|
||||||
|
* templates with long SVG path definitions.
|
||||||
|
*
|
||||||
|
* @author Matthew Raymer
|
||||||
|
* @version 1.0.0
|
||||||
|
* @since 2024
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
name: "IconRenderer",
|
||||||
|
})
|
||||||
|
export default class IconRenderer extends Vue {
|
||||||
|
@Prop({ required: true }) readonly iconName!: string;
|
||||||
|
@Prop({ default: "h-5 w-5" }) readonly svgClass!: string;
|
||||||
|
@Prop({ default: "none" }) readonly fill!: string;
|
||||||
|
@Prop({ default: "currentColor" }) readonly stroke!: string;
|
||||||
|
@Prop({ default: "0 0 24 24" }) readonly viewBox!: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the icon data for the specified icon name
|
||||||
|
*
|
||||||
|
* @returns {IconData | null} The icon data object or null if not found
|
||||||
|
*/
|
||||||
|
get iconData(): IconData | null {
|
||||||
|
const icon = (icons as IconsJson)[this.iconName];
|
||||||
|
if (!icon) {
|
||||||
|
logger.warn(`Icon "${this.iconName}" not found in icons.json`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert single path to array format for consistency
|
||||||
|
if ("d" in icon) {
|
||||||
|
return {
|
||||||
|
paths: [icon as IconPath],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return icon as IconData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -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 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,12 +121,23 @@
|
|||||||
playsinline
|
playsinline
|
||||||
muted
|
muted
|
||||||
></video>
|
></video>
|
||||||
<button
|
<div
|
||||||
class="absolute bottom-4 left-1/2 -translate-x-1/2 bg-white text-slate-800 p-3 rounded-full text-2xl leading-none"
|
class="absolute bottom-4 inset-x-0 flex items-center justify-center gap-4"
|
||||||
@click="capturePhoto"
|
|
||||||
>
|
>
|
||||||
<font-awesome icon="camera" class="w-[1em]" />
|
<button
|
||||||
</button>
|
class="bg-white text-slate-800 p-3 rounded-full text-2xl leading-none"
|
||||||
|
@click="capturePhoto"
|
||||||
|
>
|
||||||
|
<font-awesome icon="camera" class="w-[1em]" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="platformCapabilities.isMobile"
|
||||||
|
class="bg-white text-slate-800 p-3 rounded-full text-2xl leading-none"
|
||||||
|
@click="rotateCamera"
|
||||||
|
>
|
||||||
|
<font-awesome icon="rotate" class="w-[1em]" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -229,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>
|
||||||
@@ -247,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>();
|
||||||
|
|
||||||
@@ -262,6 +281,11 @@ const inputImageFileNameRef = ref<Blob>();
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
defaultCameraMode: {
|
||||||
|
type: String,
|
||||||
|
default: "environment",
|
||||||
|
validator: (value: string) => ["environment", "user"].includes(value),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class ImageMethodDialog extends Vue {
|
export default class ImageMethodDialog extends Vue {
|
||||||
@@ -303,6 +327,9 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
/** Camera stream reference */
|
/** Camera stream reference */
|
||||||
private cameraStream: MediaStream | null = null;
|
private cameraStream: MediaStream | null = null;
|
||||||
|
|
||||||
|
/** Current camera facing mode */
|
||||||
|
private currentFacingMode: "environment" | "user" = "environment";
|
||||||
|
|
||||||
private platformService = PlatformServiceFactory.getInstance();
|
private platformService = PlatformServiceFactory.getInstance();
|
||||||
URL = window.URL || window.webkitURL;
|
URL = window.URL || window.webkitURL;
|
||||||
|
|
||||||
@@ -334,7 +361,10 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
*/
|
*/
|
||||||
async mounted() {
|
async 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);
|
||||||
@@ -361,15 +391,16 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
open(setImageFn: (arg: string) => void, claimType: string, crop?: boolean) {
|
open(setImageFn: (arg: string) => void, claimType: string, crop?: boolean) {
|
||||||
|
logger.debug("ImageMethodDialog.open called");
|
||||||
this.claimType = claimType;
|
this.claimType = claimType;
|
||||||
this.crop = !!crop;
|
this.crop = !!crop;
|
||||||
this.imageCallback = setImageFn;
|
this.imageCallback = setImageFn;
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
|
this.currentFacingMode = this.defaultCameraMode as "environment" | "user";
|
||||||
|
|
||||||
// Start camera preview immediately if not on mobile
|
// Start camera preview immediately
|
||||||
if (!this.platformCapabilities.isNativeApp) {
|
logger.debug("Starting camera preview from open()");
|
||||||
this.startCameraPreview();
|
this.startCameraPreview();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadImageFile(event: Event) {
|
async uploadImageFile(event: Event) {
|
||||||
@@ -438,46 +469,24 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
logger.debug("startCameraPreview called");
|
logger.debug("startCameraPreview called");
|
||||||
logger.debug("Current showCameraPreview state:", this.showCameraPreview);
|
logger.debug("Current showCameraPreview state:", this.showCameraPreview);
|
||||||
logger.debug("Platform capabilities:", this.platformCapabilities);
|
logger.debug("Platform capabilities:", this.platformCapabilities);
|
||||||
|
logger.debug("MediaDevices available:", !!navigator.mediaDevices);
|
||||||
|
logger.debug(
|
||||||
|
"getUserMedia available:",
|
||||||
|
!!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia),
|
||||||
|
);
|
||||||
|
|
||||||
if (this.platformCapabilities.isNativeApp) {
|
|
||||||
logger.debug("Using platform service for mobile device");
|
|
||||||
this.cameraState = "initializing";
|
|
||||||
this.cameraStateMessage = "Using platform camera service...";
|
|
||||||
try {
|
|
||||||
const result = await this.platformService.takePicture();
|
|
||||||
this.blob = result.blob;
|
|
||||||
this.fileName = result.fileName;
|
|
||||||
this.cameraState = "ready";
|
|
||||||
this.cameraStateMessage = "Photo captured successfully";
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error taking picture:", error);
|
|
||||||
this.cameraState = "error";
|
|
||||||
this.cameraStateMessage =
|
|
||||||
error instanceof Error ? error.message : "Failed to take picture";
|
|
||||||
this.error =
|
|
||||||
error instanceof Error ? error.message : "Failed to take picture";
|
|
||||||
this.$notify(
|
|
||||||
{
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Error",
|
|
||||||
text: "Failed to take picture. Please try again.",
|
|
||||||
},
|
|
||||||
5000,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.debug("Starting camera preview for desktop browser");
|
|
||||||
try {
|
try {
|
||||||
this.cameraState = "initializing";
|
this.cameraState = "initializing";
|
||||||
this.cameraStateMessage = "Requesting camera access...";
|
this.cameraStateMessage = "Requesting camera access...";
|
||||||
this.showCameraPreview = true;
|
this.showCameraPreview = true;
|
||||||
await this.$nextTick();
|
await this.$nextTick();
|
||||||
|
|
||||||
|
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||||
|
throw new Error("Camera API not available in this browser");
|
||||||
|
}
|
||||||
|
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
video: { facingMode: "environment" },
|
video: { facingMode: this.currentFacingMode },
|
||||||
});
|
});
|
||||||
logger.debug("Camera access granted");
|
logger.debug("Camera access granted");
|
||||||
this.cameraStream = stream;
|
this.cameraStream = stream;
|
||||||
@@ -491,25 +500,36 @@ 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
|
||||||
resolve(true);
|
.play()
|
||||||
});
|
.then(() => {
|
||||||
|
logger.debug("Video element started playing");
|
||||||
|
resolve(true);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
logger.error("Error playing video:", error);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
logger.error("Video element not found");
|
||||||
|
throw new Error("Video element not found");
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error starting camera preview:", error);
|
logger.error("Error starting camera preview:", error);
|
||||||
let errorMessage =
|
let errorMessage =
|
||||||
error instanceof Error ? error.message : "Failed to access camera";
|
error instanceof Error ? error.message : "Failed to access camera";
|
||||||
if (
|
if (
|
||||||
error.name === "NotReadableError" ||
|
error instanceof Error &&
|
||||||
error.name === "TrackStartError"
|
(error.name === "NotReadableError" || 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.name === "NotAllowedError" ||
|
error instanceof Error &&
|
||||||
error.name === "PermissionDeniedError"
|
(error.name === "NotAllowedError" ||
|
||||||
|
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.";
|
||||||
@@ -517,6 +537,7 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
this.cameraState = "error";
|
this.cameraState = "error";
|
||||||
this.cameraStateMessage = errorMessage;
|
this.cameraStateMessage = errorMessage;
|
||||||
this.error = errorMessage;
|
this.error = errorMessage;
|
||||||
|
this.showCameraPreview = false;
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -526,7 +547,6 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
},
|
},
|
||||||
5000,
|
5000,
|
||||||
);
|
);
|
||||||
this.showCameraPreview = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -578,6 +598,21 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async rotateCamera() {
|
||||||
|
// Toggle between front and back cameras
|
||||||
|
this.currentFacingMode =
|
||||||
|
this.currentFacingMode === "environment" ? "user" : "environment";
|
||||||
|
|
||||||
|
// Stop current stream
|
||||||
|
if (this.cameraStream) {
|
||||||
|
this.cameraStream.getTracks().forEach((track) => track.stop());
|
||||||
|
this.cameraStream = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start new stream with updated facing mode
|
||||||
|
await this.startCameraPreview();
|
||||||
|
}
|
||||||
|
|
||||||
private createBlobURL(blob: Blob): string {
|
private createBlobURL(blob: Blob): string {
|
||||||
return URL.createObjectURL(blob);
|
return URL.createObjectURL(blob);
|
||||||
}
|
}
|
||||||
@@ -612,6 +647,7 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
5000,
|
5000,
|
||||||
);
|
);
|
||||||
this.uploading = false;
|
this.uploading = false;
|
||||||
|
this.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
formData.append("image", this.blob, this.fileName || "photo.jpg");
|
formData.append("image", this.blob, this.fileName || "photo.jpg");
|
||||||
@@ -666,6 +702,7 @@ export default class ImageMethodDialog extends Vue {
|
|||||||
);
|
);
|
||||||
this.uploading = false;
|
this.uploading = false;
|
||||||
this.blob = undefined;
|
this.blob = undefined;
|
||||||
|
this.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -673,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 || "";
|
||||||
@@ -296,7 +301,7 @@ export default class MembersList extends Vue {
|
|||||||
this.decryptedMembers.length === 0 ||
|
this.decryptedMembers.length === 0 ||
|
||||||
this.decryptedMembers[0].member.memberId !== this.members[0].memberId
|
this.decryptedMembers[0].member.memberId !== this.members[0].memberId
|
||||||
) {
|
) {
|
||||||
return "Your password is not the same as the organizer. Reload or have them check their password.";
|
return "Your password is not the same as the organizer. Retry or have them check their password.";
|
||||||
} else {
|
} else {
|
||||||
// the first (organizer) member was decrypted OK
|
// the first (organizer) member was decrypted OK
|
||||||
return "";
|
return "";
|
||||||
@@ -337,7 +342,7 @@ export default class MembersList extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "info",
|
type: "info",
|
||||||
title: "Contact Exists",
|
title: "Contact Exists",
|
||||||
text: "They are in your contacts. If you want to remove them, you must do that from the contacts screen.",
|
text: "They are in your contacts. To remove them, use the contacts page.",
|
||||||
},
|
},
|
||||||
10000,
|
10000,
|
||||||
);
|
);
|
||||||
@@ -347,7 +352,7 @@ export default class MembersList extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "info",
|
type: "info",
|
||||||
title: "Contact Available",
|
title: "Contact Available",
|
||||||
text: "This is to add them to your contacts. If you want to remove them later, you must do that from the contacts screen.",
|
text: "This is to add them to your contacts. To remove them later, use the contacts page.",
|
||||||
},
|
},
|
||||||
10000,
|
10000,
|
||||||
);
|
);
|
||||||
@@ -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,10 @@
|
|||||||
<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 } from "../libs/endorserServer";
|
||||||
createAndSubmitOffer,
|
|
||||||
serverMessageForUser,
|
|
||||||
} 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 +114,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,11 +246,8 @@ export default class OfferDialog extends Vue {
|
|||||||
this.projectId,
|
this.projectId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (!result.success) {
|
||||||
result.type === "error" ||
|
const errorMessage = result.error;
|
||||||
this.isOfferCreationError(result.response)
|
|
||||||
) {
|
|
||||||
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(
|
||||||
{
|
{
|
||||||
@@ -289,30 +287,6 @@ export default class OfferDialog extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions for readability
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param result response "data" from the server
|
|
||||||
* @returns true if the result indicates an error
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
isOfferCreationError(result: any) {
|
|
||||||
return result.status !== 201 || result.data?.error;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
|
|
||||||
* @returns best guess at an error message
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
getOfferCreationErrorMessage(result: any) {
|
|
||||||
return (
|
|
||||||
serverMessageForUser(result) ||
|
|
||||||
result.error?.userMessage ||
|
|
||||||
result.error?.error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -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.updateDidSpecificSettings(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.updateDidSpecificSettings(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,20 +29,22 @@ 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
|
||||||
) {
|
) {
|
||||||
const didPrefix = settings.activeDid?.slice(11, 15);
|
const didPrefix = settings.activeDid?.slice(11, 15);
|
||||||
this.message = "You're linked to a non-prod server, user " + didPrefix;
|
this.message = "You're not using prod, user " + didPrefix;
|
||||||
} else if (
|
} else if (
|
||||||
settings.warnIfProdServer &&
|
settings.warnIfProdServer &&
|
||||||
settings.apiServer === AppString.PROD_ENDORSER_API_SERVER
|
settings.apiServer === AppString.PROD_ENDORSER_API_SERVER
|
||||||
) {
|
) {
|
||||||
const didPrefix = settings.activeDid?.slice(11, 15);
|
const didPrefix = settings.activeDid?.slice(11, 15);
|
||||||
this.message =
|
this.message = "You are using prod, user " + didPrefix;
|
||||||
"You're linked to the production server, user " + didPrefix;
|
|
||||||
}
|
}
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -1,19 +1,47 @@
|
|||||||
import migrationService from "../services/migrationService";
|
import {
|
||||||
import type { QueryExecResult, SqlValue } from "../interfaces/database";
|
registerMigration,
|
||||||
|
runMigrations as runMigrationsService,
|
||||||
|
} 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)
|
// Each migration can include multiple SQL statements (with semicolons)
|
||||||
const MIGRATIONS = [
|
const MIGRATIONS = [
|
||||||
{
|
{
|
||||||
name: "001_initial",
|
name: "001_initial",
|
||||||
// see ../db/tables files for explanations of the fields
|
|
||||||
sql: `
|
sql: `
|
||||||
CREATE TABLE IF NOT EXISTS accounts (
|
CREATE TABLE IF NOT EXISTS accounts (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
dateCreated TEXT NOT NULL,
|
dateCreated TEXT NOT NULL,
|
||||||
derivationPath TEXT,
|
derivationPath TEXT,
|
||||||
did TEXT NOT NULL,
|
did TEXT NOT NULL,
|
||||||
identity TEXT,
|
identityEncrBase64 TEXT, -- encrypted & base64-encoded
|
||||||
mnemonic TEXT,
|
mnemonicEncrBase64 TEXT, -- encrypted & base64-encoded
|
||||||
passkeyCredIdHex TEXT,
|
passkeyCredIdHex TEXT,
|
||||||
publicKeyHex TEXT NOT NULL
|
publicKeyHex TEXT NOT NULL
|
||||||
);
|
);
|
||||||
@@ -22,9 +50,11 @@ const MIGRATIONS = [
|
|||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS secret (
|
CREATE TABLE IF NOT EXISTS secret (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
secret TEXT NOT NULL
|
secretBase64 TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
INSERT INTO secret (id, secretBase64) VALUES (1, '${secretBase64}');
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS settings (
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
accountDid TEXT,
|
accountDid TEXT,
|
||||||
@@ -59,6 +89,8 @@ const MIGRATIONS = [
|
|||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_settings_accountDid ON settings(accountDid);
|
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 (
|
CREATE TABLE IF NOT EXISTS contacts (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
did TEXT NOT NULL,
|
did TEXT NOT NULL,
|
||||||
@@ -76,7 +108,7 @@ const MIGRATIONS = [
|
|||||||
CREATE INDEX IF NOT EXISTS idx_contacts_name ON contacts(name);
|
CREATE INDEX IF NOT EXISTS idx_contacts_name ON contacts(name);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS logs (
|
CREATE TABLE IF NOT EXISTS logs (
|
||||||
date TEXT PRIMARY KEY,
|
date TEXT NOT NULL,
|
||||||
message TEXT NOT NULL
|
message TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -86,21 +118,25 @@ const MIGRATIONS = [
|
|||||||
);
|
);
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "002_add_iViewContent_to_contacts",
|
||||||
|
sql: `
|
||||||
|
ALTER TABLE contacts ADD COLUMN iViewContent BOOLEAN DEFAULT TRUE;
|
||||||
|
`,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export async function registerMigrations(): Promise<void> {
|
/**
|
||||||
// Register all migrations
|
* @param sqlExec - A function that executes a SQL statement and returns the result
|
||||||
for (const migration of MIGRATIONS) {
|
* @param extractMigrationNames - A function that extracts the names (string array) from "select name from migrations"
|
||||||
await migrationService.registerMigration(migration);
|
*/
|
||||||
}
|
export async function runMigrations<T>(
|
||||||
}
|
sqlExec: (sql: string, params?: unknown[]) => Promise<unknown>,
|
||||||
|
sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
|
||||||
export async function runMigrations(
|
extractMigrationNames: (result: T) => Set<string>,
|
||||||
sqlExec: (
|
|
||||||
sql: string,
|
|
||||||
params?: SqlValue[],
|
|
||||||
) => Promise<Array<QueryExecResult>>,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await registerMigrations();
|
for (const migration of MIGRATIONS) {
|
||||||
await migrationService.runMigrations(sqlExec);
|
registerMigration(migration);
|
||||||
|
}
|
||||||
|
await runMigrationsService(sqlExec, sqlQuery, extractMigrationNames);
|
||||||
}
|
}
|
||||||
|
|||||||
462
src/db/databaseUtil.ts
Normal file
@@ -0,0 +1,462 @@
|
|||||||
|
/**
|
||||||
|
* 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 insertDidSpecificSettings(
|
||||||
|
did: string,
|
||||||
|
settings: Partial<Settings> = {},
|
||||||
|
): Promise<boolean> {
|
||||||
|
const platform = PlatformServiceFactory.getInstance();
|
||||||
|
const { sql, params } = generateInsertStatement(
|
||||||
|
{ ...settings, accountDid: did }, // make sure accountDid is set to the given value
|
||||||
|
"settings",
|
||||||
|
);
|
||||||
|
const result = await platform.dbExec(sql, params);
|
||||||
|
return result.changes === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateDidSpecificSettings(
|
||||||
|
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) {
|
||||||
|
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) {
|
||||||
|
// we created DID-specific settings when generated or imported, so this shouldn't happen
|
||||||
|
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) {
|
||||||
|
settings.searchBoxes = parseJsonField(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 SQL INSERT statement and parameters from a model object
|
||||||
|
*
|
||||||
|
* This helper function creates a parameterized SQL INSERT statement
|
||||||
|
* from a JavaScript object. It filters out undefined values and
|
||||||
|
* creates the appropriate SQL syntax with placeholders.
|
||||||
|
*
|
||||||
|
* The function is used internally by the migration functions to
|
||||||
|
* safely insert data into the SQLite database.
|
||||||
|
*
|
||||||
|
* @function generateInsertStatement
|
||||||
|
* @param {Record<string, unknown>} model - The model object containing fields to insert
|
||||||
|
* @param {string} tableName - The name of the table to insert into
|
||||||
|
* @returns {Object} Object containing the SQL statement and parameters array
|
||||||
|
* @returns {string} returns.sql - The SQL INSERT statement
|
||||||
|
* @returns {unknown[]} returns.params - Array of parameter values
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const contact = { did: 'did:example:123', name: 'John Doe' };
|
||||||
|
* const { sql, params } = generateInsertStatement(contact, 'contacts');
|
||||||
|
* // sql: "INSERT INTO contacts (did, name) VALUES (?, ?)"
|
||||||
|
* // params: ['did:example:123', 'John Doe']
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
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 SQL UPDATE statement and parameters from a model object
|
||||||
|
*
|
||||||
|
* This helper function creates a parameterized SQL UPDATE statement
|
||||||
|
* from a JavaScript object. It filters out undefined values and
|
||||||
|
* creates the appropriate SQL syntax with placeholders.
|
||||||
|
*
|
||||||
|
* The function is used internally by the migration functions to
|
||||||
|
* safely update data in the SQLite database.
|
||||||
|
*
|
||||||
|
* @function generateUpdateStatement
|
||||||
|
* @param {Record<string, unknown>} model - The model object containing fields to update
|
||||||
|
* @param {string} tableName - The name of the table to update
|
||||||
|
* @param {string} whereClause - The WHERE clause for the update (e.g. "id = ?")
|
||||||
|
* @param {unknown[]} [whereParams=[]] - Parameters for the WHERE clause
|
||||||
|
* @returns {Object} Object containing the SQL statement and parameters array
|
||||||
|
* @returns {string} returns.sql - The SQL UPDATE statement
|
||||||
|
* @returns {unknown[]} returns.params - Array of parameter values
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const contact = { name: 'Jane Doe' };
|
||||||
|
* const { sql, params } = generateUpdateStatement(contact, 'contacts', 'did = ?', ['did:example:123']);
|
||||||
|
* // sql: "UPDATE contacts SET name = ? WHERE did = ?"
|
||||||
|
* // params: ['Jane Doe', 'did:example:123']
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debug function to inspect raw settings data in the database
|
||||||
|
* This helps diagnose issues with data corruption or malformed JSON
|
||||||
|
* @param did Optional DID to inspect specific account settings
|
||||||
|
* @author Matthew Raymer
|
||||||
|
*/
|
||||||
|
export async function debugSettingsData(did?: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const platform = PlatformServiceFactory.getInstance();
|
||||||
|
|
||||||
|
// Get all settings records
|
||||||
|
const allSettings = await platform.dbQuery("SELECT * FROM settings");
|
||||||
|
|
||||||
|
logConsoleAndDb(
|
||||||
|
`[DEBUG] Total settings records: ${allSettings?.values?.length || 0}`,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (allSettings?.values?.length) {
|
||||||
|
allSettings.values.forEach((row, index) => {
|
||||||
|
const settings = mapColumnsToValues(allSettings.columns, [row])[0];
|
||||||
|
logConsoleAndDb(`[DEBUG] Settings record ${index + 1}:`, false);
|
||||||
|
logConsoleAndDb(`[DEBUG] - ID: ${settings.id}`, false);
|
||||||
|
logConsoleAndDb(`[DEBUG] - accountDid: ${settings.accountDid}`, false);
|
||||||
|
logConsoleAndDb(`[DEBUG] - activeDid: ${settings.activeDid}`, false);
|
||||||
|
|
||||||
|
if (settings.searchBoxes) {
|
||||||
|
logConsoleAndDb(
|
||||||
|
`[DEBUG] - searchBoxes type: ${typeof settings.searchBoxes}`,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
logConsoleAndDb(
|
||||||
|
`[DEBUG] - searchBoxes value: ${String(settings.searchBoxes)}`,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Try to parse it
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(String(settings.searchBoxes));
|
||||||
|
logConsoleAndDb(
|
||||||
|
`[DEBUG] - searchBoxes parsed successfully: ${JSON.stringify(parsed)}`,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
} catch (parseError) {
|
||||||
|
logConsoleAndDb(
|
||||||
|
`[DEBUG] - searchBoxes parse error: ${parseError}`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logConsoleAndDb(
|
||||||
|
`[DEBUG] - Full record: ${JSON.stringify(settings, null, 2)}`,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If specific DID provided, also check accounts table
|
||||||
|
if (did) {
|
||||||
|
const account = await platform.dbQuery(
|
||||||
|
"SELECT * FROM accounts WHERE did = ?",
|
||||||
|
[did],
|
||||||
|
);
|
||||||
|
logConsoleAndDb(
|
||||||
|
`[DEBUG] Account for ${did}: ${JSON.stringify(account, null, 2)}`,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logConsoleAndDb(`[DEBUG] Error inspecting settings data: ${error}`, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Platform-agnostic JSON parsing utility
|
||||||
|
* Handles different SQLite implementations:
|
||||||
|
* - Web SQLite (wa-sqlite/absurd-sql): Auto-parses JSON strings to objects
|
||||||
|
* - Capacitor SQLite: Returns raw strings that need manual parsing
|
||||||
|
*
|
||||||
|
* @param value The value to parse (could be string or already parsed object)
|
||||||
|
* @param defaultValue Default value if parsing fails
|
||||||
|
* @returns Parsed object or default value
|
||||||
|
* @author Matthew Raymer
|
||||||
|
*/
|
||||||
|
export function parseJsonField<T>(value: unknown, defaultValue: T): T {
|
||||||
|
try {
|
||||||
|
// If already an object (web SQLite auto-parsed), return as-is
|
||||||
|
if (typeof value === "object" && value !== null) {
|
||||||
|
return value as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a string (Capacitor SQLite or fallback), parse it
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return JSON.parse(value) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's null/undefined, return default
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue;
|
||||||
|
} catch (error) {
|
||||||
|
logConsoleAndDb(
|
||||||
|
`[databaseUtil] Failed to parse JSON field: ${error}`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Check the contact & settings export to see whether you want your new table to be included in it.
|
# Check the contact & settings export to see whether you want your new table to be included in it
|
||||||
|
|||||||