Compare commits
162 Commits
sql-absurd
...
0.5.8
| Author | SHA1 | Date | |
|---|---|---|---|
| 3118f71320 | |||
| d12f23aa81 | |||
| e9a8a3c1e7 | |||
| 1e0efe6011 | |||
| 16557f1e4b | |||
| c4a54967bc | |||
| 20ade415dc | |||
| 6689520270 | |||
| 3fd6c2b80d | |||
| a5c5c2b9dd | |||
| cf33a39fbc | |||
| 8629cefa13 | |||
| 5e851e442f | |||
| 4a43bc9c6c | |||
| 60de8cee62 | |||
|
|
bb2a4ab76e | ||
|
|
048dded278 | ||
| e240c2940a | |||
| 54dca9e745 | |||
| 9f0fed0a60 | |||
| 0d152adbf2 | |||
| cead308800 | |||
| 676a301331 | |||
| d6db81cc36 | |||
|
|
f2ddcd2541 | ||
| fb81f7b96e | |||
| a23416ead1 | |||
| 530c7c1a13 | |||
| 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 | ||
| 1b7c96ed9b | |||
| 41365fab8f | |||
| 5cc42be58a | |||
| 3d1a2eeb8d | |||
| 7b0ee2e44e | |||
| ac018997e8 | |||
| 6f449e9c1f | |||
| 543599a6a1 |
@@ -2,11 +2,12 @@
|
||||
|
||||
# iOS doesn't like spaces in the app title.
|
||||
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).
|
||||
VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F
|
||||
VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000
|
||||
# Using shared server by default to ease setup, which works for shared test users.
|
||||
VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app
|
||||
VITE_DEFAULT_PARTNER_API_SERVER=http://localhost:3000
|
||||
#VITE_DEFAULT_PUSH_SERVER... can't be set up with localhost domain
|
||||
VITE_PASSKEYS_ENABLED=true
|
||||
|
||||
@@ -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_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_PARTNER_API_SERVER=https://test-partner-api.endorser.ch
|
||||
VITE_DEFAULT_PUSH_SERVER=https://test.timesafari.app
|
||||
VITE_PASSKEYS_ENABLED=true
|
||||
|
||||
@@ -4,6 +4,12 @@ module.exports = {
|
||||
node: true,
|
||||
es2022: true,
|
||||
},
|
||||
ignorePatterns: [
|
||||
'node_modules/',
|
||||
'dist/',
|
||||
'dist-electron/',
|
||||
'*.d.ts'
|
||||
],
|
||||
extends: [
|
||||
"plugin:vue/vue3-recommended",
|
||||
"eslint:recommended",
|
||||
|
||||
6
.gitignore
vendored
@@ -51,6 +51,8 @@ vendor/
|
||||
# Build logs
|
||||
build_logs/
|
||||
|
||||
android/app/src/main/assets/public
|
||||
android/app/src/main/res
|
||||
# PWA icon files generated by capacitor-assets
|
||||
icons
|
||||
|
||||
|
||||
android/app/src/main/res/
|
||||
87
BUILDING.md
@@ -9,19 +9,6 @@ For a quick dev environment setup, use [pkgx](https://pkgx.dev).
|
||||
- Node.js (LTS version recommended)
|
||||
- npm (comes with Node.js)
|
||||
- Git
|
||||
- For Android builds: Android Studio with SDK installed
|
||||
- For iOS builds: macOS with Xcode and ruby gems & bundle
|
||||
- `pkgx +rubygems.org sh`
|
||||
|
||||
- ... and you may have to fix these, especially with pkgx
|
||||
|
||||
```bash
|
||||
gem_path=$(which gem)
|
||||
shortened_path="${gem_path:h:h}"
|
||||
export GEM_HOME=$shortened_path
|
||||
export GEM_PATH=$shortened_path
|
||||
```
|
||||
|
||||
- For desktop builds: Additional build tools based on your OS
|
||||
|
||||
## Forks
|
||||
@@ -54,6 +41,7 @@ Install dependencies:
|
||||
1. Run the production build:
|
||||
|
||||
```bash
|
||||
rm -rf dist
|
||||
npm run build:web
|
||||
```
|
||||
|
||||
@@ -77,6 +65,8 @@ Install dependencies:
|
||||
|
||||
* Commit everything (since the commit hash is used the app).
|
||||
|
||||
* Run a build to make sure package-lock version is updated, linting works, etc: `npm install && npm run build`
|
||||
|
||||
* Put the commit hash in the changelog (which will help you remember to bump the version 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`.
|
||||
@@ -84,7 +74,7 @@ Install dependencies:
|
||||
* For test, build the app (because test server is not yet set up to build):
|
||||
|
||||
```bash
|
||||
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_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:
|
||||
@@ -326,17 +316,33 @@ npm run build:electron-prod && npm run electron:start
|
||||
|
||||
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
|
||||
rm -rf dist
|
||||
npm run build:web
|
||||
npm run build:capacitor
|
||||
```
|
||||
|
||||
2. Update iOS project with latest build:
|
||||
|
||||
```bash
|
||||
npx cap sync ios
|
||||
```
|
||||
|
||||
@@ -345,19 +351,22 @@ Prerequisites: macOS with Xcode installed
|
||||
3. Copy the assets:
|
||||
|
||||
```bash
|
||||
# It makes no sense why capacitor-assets will not run without these but it actually changes the contents.
|
||||
mkdir -p ios/App/App/Assets.xcassets/AppIcon.appiconset
|
||||
echo '{"images":[]}' > ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json
|
||||
mkdir -p ios/App/App/Assets.xcassets/Splash.imageset
|
||||
echo '{"images":[]}' > ios/App/App/Assets.xcassets/Splash.imageset/Contents.json
|
||||
npx capacitor-assets generate --ios
|
||||
```
|
||||
|
||||
4. Bump the version to match Android:
|
||||
4. Bump the version to match Android & package.json:
|
||||
|
||||
```
|
||||
cd ios/App
|
||||
xcrun agvtool new-version 15
|
||||
xcrun agvtool new-version 34
|
||||
# Unfortunately this edits Info.plist directly.
|
||||
#xcrun agvtool new-marketing-version 0.4.5
|
||||
cat App.xcodeproj/project.pbxproj | sed "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 0.4.5;/g" > temp
|
||||
mv temp App.xcodeproj/project.pbxproj
|
||||
cat App.xcodeproj/project.pbxproj | sed "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 0.5.8;/g" > temp && mv temp App.xcodeproj/project.pbxproj
|
||||
cd -
|
||||
```
|
||||
|
||||
@@ -369,28 +378,25 @@ Prerequisites: macOS with Xcode installed
|
||||
|
||||
6. Use Xcode to build and run on simulator or device.
|
||||
|
||||
* Select Product -> Destination with some Simulator version. Then click the run arrow.
|
||||
|
||||
7. Release
|
||||
|
||||
* Under "General" renamed a bunch of things to "Time Safari"
|
||||
* Choose Product -> Destination -> Build Any iOS
|
||||
* Someday: Under "General" we want to rename a bunch of things to "Time Safari"
|
||||
* Choose Product -> Destination -> Any iOS Device
|
||||
* 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`).
|
||||
* Click Distribute -> App Store Connect
|
||||
* In AppStoreConnect, add the build to the distribution: remove the current build with the "-" when you hover over it, then "Add Build" with the new build.
|
||||
* May have to go to App Review, click Submission, then hover over the build and click "-".
|
||||
* It can take 15 minutes for the build to show up in the list of builds.
|
||||
* You'll probably have to "Manage" something about encryption, disallowed in France.
|
||||
* Then "Save" and "Add to Review" and "Resubmit to App Review".
|
||||
|
||||
#### First-time iOS Configuration
|
||||
|
||||
- Generate certificates inside XCode.
|
||||
|
||||
- Right-click on App and under Signing & Capabilities set the Team.
|
||||
|
||||
### Android Build
|
||||
|
||||
Prerequisites: Android Studio with SDK installed
|
||||
Prerequisites: Android Studio with Java SDK installed
|
||||
|
||||
1. Build the web assets:
|
||||
|
||||
@@ -412,7 +418,7 @@ Prerequisites: Android Studio with SDK installed
|
||||
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:
|
||||
|
||||
@@ -429,7 +435,6 @@ Prerequisites: Android Studio with SDK installed
|
||||
./gradlew clean
|
||||
./gradlew build -Dlint.baselines.continue=true
|
||||
cd -
|
||||
npx cap run android
|
||||
```
|
||||
|
||||
... or, to create the `aab` file, `bundle` instead of `build`:
|
||||
@@ -445,7 +450,9 @@ Prerequisites: Android Studio with SDK installed
|
||||
* Then `bundleRelease`:
|
||||
|
||||
```bash
|
||||
cd android
|
||||
./gradlew bundleRelease -Dlint.baselines.continue=true
|
||||
cd -
|
||||
```
|
||||
|
||||
... and find your `aab` file at app/build/outputs/bundle/release
|
||||
@@ -458,8 +465,10 @@ At play.google.com/console:
|
||||
- Hit "Next".
|
||||
- Save, go to "Publishing Overview" as prompted, and click "Send changes for review".
|
||||
|
||||
- Note that if you add testers, you have to go to "Publishing Overview" and send those changes or your (closed) testers won't see it.
|
||||
|
||||
## First-time Android Configuration for deep links
|
||||
|
||||
## Android Configuration for deep links
|
||||
|
||||
You must add the following intent filter to the `android/app/src/main/AndroidManifest.xml` file:
|
||||
|
||||
@@ -470,4 +479,6 @@ You must add the following intent filter to the `android/app/src/main/AndroidMan
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="timesafari" />
|
||||
</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]
|
||||
|
||||
14
CHANGELOG.md
@@ -6,6 +6,20 @@ 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).
|
||||
|
||||
|
||||
## [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
|
||||
### Added
|
||||
|
||||
@@ -31,8 +31,8 @@ android {
|
||||
applicationId "app.timesafari.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 18
|
||||
versionName "0.4.7"
|
||||
versionCode 34
|
||||
versionName "0.5.8"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// 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.core:core-splashscreen:$coreSplashScreenVersion"
|
||||
implementation project(':capacitor-android')
|
||||
implementation project(':capacitor-community-sqlite')
|
||||
implementation "androidx.biometric:biometric:1.2.0-alpha05"
|
||||
testImplementation "junit:junit:$junitVersion"
|
||||
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
||||
|
||||
@@ -9,6 +9,7 @@ android {
|
||||
|
||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||
dependencies {
|
||||
implementation project(':capacitor-community-sqlite')
|
||||
implementation project(':capacitor-mlkit-barcode-scanning')
|
||||
implementation project(':capacitor-app')
|
||||
implementation project(':capacitor-camera')
|
||||
|
||||
@@ -16,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",
|
||||
"classpath": "io.capawesome.capacitorjs.plugins.mlkit.barcodescanning.BarcodeScannerPlugin"
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
package app.timesafari;
|
||||
|
||||
import android.os.Bundle;
|
||||
import com.getcapacitor.BridgeActivity;
|
||||
//import com.getcapacitor.community.sqlite.SQLite;
|
||||
|
||||
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"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background>
|
||||
<inset android:drawable="@mipmap/ic_launcher_background" android:inset="16.7%" />
|
||||
</background>
|
||||
<foreground>
|
||||
<inset android:drawable="@mipmap/ic_launcher_foreground" android:inset="16.7%" />
|
||||
</foreground>
|
||||
</adaptive-icon>
|
||||
@@ -1,5 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background>
|
||||
<inset android:drawable="@mipmap/ic_launcher_background" android:inset="16.7%" />
|
||||
</background>
|
||||
<foreground>
|
||||
<inset android:drawable="@mipmap/ic_launcher_foreground" android:inset="16.7%" />
|
||||
</foreground>
|
||||
</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'
|
||||
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
||||
|
||||
include ':capacitor-community-sqlite'
|
||||
project(':capacitor-community-sqlite').projectDir = new File('../node_modules/@capacitor-community/sqlite/android')
|
||||
|
||||
include ':capacitor-mlkit-barcode-scanning'
|
||||
project(':capacitor-mlkit-barcode-scanning').projectDir = new File('../node_modules/@capacitor-mlkit/barcode-scanning/android')
|
||||
|
||||
|
||||
|
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/services/deepLinks.ts`: Deep link processing service
|
||||
- `src/main.capacitor.ts`: Capacitor integration
|
||||
- `src/views/DeepLinkRedirectView.vue`: Page to handle links to both mobile and web
|
||||
|
||||
## Type Safety Examples
|
||||
|
||||
|
||||
@@ -3,45 +3,46 @@
|
||||
## Core Services
|
||||
|
||||
### 1. Storage Service Layer
|
||||
- [ ] Create base `StorageService` interface
|
||||
- [ ] Define common methods for all platforms
|
||||
- [ ] Add platform-specific method signatures
|
||||
- [ ] Include error handling types
|
||||
- [ ] Add migration support methods
|
||||
- [x] Create base `PlatformService` interface
|
||||
- [x] Define common methods for all platforms
|
||||
- [x] Add platform-specific method signatures
|
||||
- [x] Include error handling types
|
||||
- [x] Add migration support methods
|
||||
|
||||
- [ ] Implement platform-specific services
|
||||
- [ ] `WebSQLiteService` (absurd-sql)
|
||||
- [ ] Database initialization
|
||||
- [ ] VFS setup with IndexedDB backend
|
||||
- [ ] Connection management
|
||||
- [ ] Query builder
|
||||
- [ ] `NativeSQLiteService` (iOS/Android)
|
||||
- [x] Implement platform-specific services
|
||||
- [x] `AbsurdSqlDatabaseService` (web)
|
||||
- [x] Database initialization
|
||||
- [x] VFS setup with IndexedDB backend
|
||||
- [x] Connection management
|
||||
- [x] Operation queuing
|
||||
- [ ] `NativeSQLiteService` (iOS/Android) (planned)
|
||||
- [ ] SQLCipher integration
|
||||
- [ ] Native bridge setup
|
||||
- [ ] File system access
|
||||
- [ ] `ElectronSQLiteService`
|
||||
- [ ] `ElectronSQLiteService` (planned)
|
||||
- [ ] Node SQLite integration
|
||||
- [ ] IPC communication
|
||||
- [ ] File system access
|
||||
|
||||
### 2. Migration Services
|
||||
- [ ] Implement `MigrationService`
|
||||
- [ ] Backup creation
|
||||
- [ ] Data verification
|
||||
- [ ] Rollback procedures
|
||||
- [ ] Progress tracking
|
||||
- [ ] Create `MigrationUI` components
|
||||
- [x] Implement basic migration support
|
||||
- [x] Dual-storage pattern (SQLite + Dexie)
|
||||
- [x] Basic data verification
|
||||
- [ ] Rollback procedures (planned)
|
||||
- [ ] Progress tracking (planned)
|
||||
- [ ] Create `MigrationUI` components (planned)
|
||||
- [ ] Progress indicators
|
||||
- [ ] Error handling
|
||||
- [ ] User notifications
|
||||
- [ ] Manual triggers
|
||||
|
||||
### 3. Security Layer
|
||||
- [ ] Implement `EncryptionService`
|
||||
- [x] Basic data integrity
|
||||
- [ ] Implement `EncryptionService` (planned)
|
||||
- [ ] Key management
|
||||
- [ ] Encryption/decryption
|
||||
- [ ] Secure storage
|
||||
- [ ] Add `BiometricService`
|
||||
- [ ] Add `BiometricService` (planned)
|
||||
- [ ] Platform detection
|
||||
- [ ] Authentication flow
|
||||
- [ ] Fallback mechanisms
|
||||
@@ -49,18 +50,19 @@
|
||||
## Platform-Specific Implementation
|
||||
|
||||
### Web Platform
|
||||
- [ ] Setup absurd-sql
|
||||
- [ ] Install dependencies
|
||||
- [x] Setup absurd-sql
|
||||
- [x] Install dependencies
|
||||
```json
|
||||
{
|
||||
"@jlongster/sql.js": "^1.8.0",
|
||||
"absurd-sql": "^1.8.0"
|
||||
}
|
||||
```
|
||||
- [ ] Configure VFS with IndexedDB backend
|
||||
- [ ] Setup worker threads
|
||||
- [ ] Implement connection pooling
|
||||
- [ ] Configure database pragmas
|
||||
- [x] Configure VFS with IndexedDB backend
|
||||
- [x] Setup worker threads
|
||||
- [x] Implement operation queuing
|
||||
- [x] Configure database pragmas
|
||||
|
||||
```sql
|
||||
PRAGMA journal_mode=MEMORY;
|
||||
PRAGMA synchronous=NORMAL;
|
||||
@@ -68,19 +70,19 @@
|
||||
PRAGMA busy_timeout=5000;
|
||||
```
|
||||
|
||||
- [ ] Update build configuration
|
||||
- [ ] Modify `vite.config.ts`
|
||||
- [ ] Add worker configuration
|
||||
- [ ] Update chunk splitting
|
||||
- [ ] Configure asset handling
|
||||
- [x] Update build configuration
|
||||
- [x] Modify `vite.config.ts`
|
||||
- [x] Add worker configuration
|
||||
- [x] Update chunk splitting
|
||||
- [x] Configure asset handling
|
||||
|
||||
- [ ] Implement IndexedDB fallback
|
||||
- [ ] Create fallback service
|
||||
- [ ] Add data synchronization
|
||||
- [ ] Handle quota exceeded
|
||||
- [ ] Implement atomic operations
|
||||
- [x] Implement IndexedDB backend
|
||||
- [x] Create database service
|
||||
- [x] Add operation queuing
|
||||
- [x] Handle initialization
|
||||
- [x] Implement atomic operations
|
||||
|
||||
### iOS Platform
|
||||
### iOS Platform (Planned)
|
||||
- [ ] Setup SQLCipher
|
||||
- [ ] Install pod dependencies
|
||||
- [ ] Configure encryption
|
||||
@@ -93,7 +95,7 @@
|
||||
- [ ] Configure backup
|
||||
- [ ] Setup app groups
|
||||
|
||||
### Android Platform
|
||||
### Android Platform (Planned)
|
||||
- [ ] Setup SQLCipher
|
||||
- [ ] Add Gradle dependencies
|
||||
- [ ] Configure encryption
|
||||
@@ -106,7 +108,7 @@
|
||||
- [ ] Configure backup
|
||||
- [ ] Setup file provider
|
||||
|
||||
### Electron Platform
|
||||
### Electron Platform (Planned)
|
||||
- [ ] Setup Node SQLite
|
||||
- [ ] Install dependencies
|
||||
- [ ] Configure IPC
|
||||
@@ -122,7 +124,8 @@
|
||||
## Data Models and Types
|
||||
|
||||
### 1. Database Schema
|
||||
- [ ] Define tables
|
||||
- [x] Define tables
|
||||
|
||||
```sql
|
||||
-- Accounts table
|
||||
CREATE TABLE accounts (
|
||||
@@ -155,13 +158,14 @@
|
||||
CREATE INDEX idx_settings_updated_at ON settings(updated_at);
|
||||
```
|
||||
|
||||
- [ ] Create indexes
|
||||
- [ ] Define constraints
|
||||
- [ ] Add triggers
|
||||
- [ ] Setup migrations
|
||||
- [x] Create indexes
|
||||
- [x] Define constraints
|
||||
- [ ] Add triggers (planned)
|
||||
- [ ] Setup migrations (planned)
|
||||
|
||||
### 2. Type Definitions
|
||||
- [ ] Create interfaces
|
||||
|
||||
- [x] Create interfaces
|
||||
```typescript
|
||||
interface Account {
|
||||
did: string;
|
||||
@@ -185,28 +189,28 @@
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] Add validation
|
||||
- [ ] Create DTOs
|
||||
- [ ] Define enums
|
||||
- [ ] Add type guards
|
||||
- [x] Add validation
|
||||
- [x] Create DTOs
|
||||
- [x] Define enums
|
||||
- [x] Add type guards
|
||||
|
||||
## UI Components
|
||||
|
||||
### 1. Migration UI
|
||||
### 1. Migration UI (Planned)
|
||||
- [ ] Create components
|
||||
- [ ] `MigrationProgress.vue`
|
||||
- [ ] `MigrationError.vue`
|
||||
- [ ] `MigrationSettings.vue`
|
||||
- [ ] `MigrationStatus.vue`
|
||||
|
||||
### 2. Settings UI
|
||||
### 2. Settings UI (Planned)
|
||||
- [ ] Update components
|
||||
- [ ] Add storage settings
|
||||
- [ ] Add migration controls
|
||||
- [ ] Add backup options
|
||||
- [ ] Add security settings
|
||||
|
||||
### 3. Error Handling UI
|
||||
### 3. Error Handling UI (Planned)
|
||||
- [ ] Create components
|
||||
- [ ] `StorageError.vue`
|
||||
- [ ] `QuotaExceeded.vue`
|
||||
@@ -216,20 +220,20 @@
|
||||
## Testing
|
||||
|
||||
### 1. Unit Tests
|
||||
- [ ] Test services
|
||||
- [ ] Storage service tests
|
||||
- [ ] Migration service tests
|
||||
- [ ] Security service tests
|
||||
- [ ] Platform detection tests
|
||||
- [x] Basic service tests
|
||||
- [x] Platform service tests
|
||||
- [x] Database operation tests
|
||||
- [ ] Security service tests (planned)
|
||||
- [ ] Platform detection tests (planned)
|
||||
|
||||
### 2. Integration Tests
|
||||
### 2. Integration Tests (Planned)
|
||||
- [ ] Test migrations
|
||||
- [ ] Web platform tests
|
||||
- [ ] iOS platform tests
|
||||
- [ ] Android platform tests
|
||||
- [ ] Electron platform tests
|
||||
|
||||
### 3. E2E Tests
|
||||
### 3. E2E Tests (Planned)
|
||||
- [ ] Test workflows
|
||||
- [ ] Account management
|
||||
- [ ] Settings management
|
||||
@@ -239,12 +243,12 @@
|
||||
## Documentation
|
||||
|
||||
### 1. Technical Documentation
|
||||
- [ ] Update architecture docs
|
||||
- [ ] Add API documentation
|
||||
- [ ] Create migration guides
|
||||
- [ ] Document security measures
|
||||
- [x] Update architecture docs
|
||||
- [x] Add API documentation
|
||||
- [ ] Create migration guides (planned)
|
||||
- [ ] Document security measures (planned)
|
||||
|
||||
### 2. User Documentation
|
||||
### 2. User Documentation (Planned)
|
||||
- [ ] Update user guides
|
||||
- [ ] Add troubleshooting guides
|
||||
- [ ] Create FAQ
|
||||
@@ -253,18 +257,18 @@
|
||||
## Deployment
|
||||
|
||||
### 1. Build Process
|
||||
- [ ] Update build scripts
|
||||
- [ ] Add platform-specific builds
|
||||
- [ ] Configure CI/CD
|
||||
- [ ] Setup automated testing
|
||||
- [x] Update build scripts
|
||||
- [x] Add platform-specific builds
|
||||
- [ ] Configure CI/CD (planned)
|
||||
- [ ] Setup automated testing (planned)
|
||||
|
||||
### 2. Release Process
|
||||
### 2. Release Process (Planned)
|
||||
- [ ] Create release checklist
|
||||
- [ ] Add version management
|
||||
- [ ] Setup rollback procedures
|
||||
- [ ] Configure monitoring
|
||||
|
||||
## Monitoring and Analytics
|
||||
## Monitoring and Analytics (Planned)
|
||||
|
||||
### 1. Error Tracking
|
||||
- [ ] Setup error logging
|
||||
@@ -278,7 +282,7 @@
|
||||
- [ ] Monitor performance
|
||||
- [ ] Collect user feedback
|
||||
|
||||
## Security Audit
|
||||
## Security Audit (Planned)
|
||||
|
||||
### 1. Code Review
|
||||
- [ ] Review encryption
|
||||
@@ -295,29 +299,31 @@
|
||||
## Success Criteria
|
||||
|
||||
### 1. Performance
|
||||
- [ ] Query response time < 100ms
|
||||
- [ ] Migration time < 5s per 1000 records
|
||||
- [ ] Storage overhead < 10%
|
||||
- [ ] Memory usage < 50MB
|
||||
- [ ] Atomic operations complete successfully
|
||||
- [ ] Transaction performance meets requirements
|
||||
- [x] Query response time < 100ms
|
||||
- [x] Operation queuing for thread safety
|
||||
- [x] Proper initialization handling
|
||||
- [ ] Migration time < 5s per 1000 records (planned)
|
||||
- [ ] Storage overhead < 10% (planned)
|
||||
- [ ] Memory usage < 50MB (planned)
|
||||
|
||||
### 2. Reliability
|
||||
- [ ] 99.9% uptime
|
||||
- [ ] Zero data loss
|
||||
- [ ] Automatic recovery
|
||||
- [ ] Backup verification
|
||||
- [ ] Transaction atomicity
|
||||
- [ ] Data consistency
|
||||
- [x] Basic data integrity
|
||||
- [x] Operation queuing
|
||||
- [ ] Automatic recovery (planned)
|
||||
- [ ] Backup verification (planned)
|
||||
- [ ] Transaction atomicity (planned)
|
||||
- [ ] Data consistency (planned)
|
||||
|
||||
### 3. Security
|
||||
- [ ] AES-256 encryption
|
||||
- [ ] Secure key storage
|
||||
- [ ] Access control
|
||||
- [ ] Audit logging
|
||||
- [x] Basic data integrity
|
||||
- [ ] AES-256 encryption (planned)
|
||||
- [ ] Secure key storage (planned)
|
||||
- [ ] Access control (planned)
|
||||
- [ ] Audit logging (planned)
|
||||
|
||||
### 4. User Experience
|
||||
- [ ] Smooth migration
|
||||
- [ ] Clear error messages
|
||||
- [ ] Progress indicators
|
||||
- [ ] Recovery options
|
||||
- [x] Basic database operations
|
||||
- [ ] Smooth migration (planned)
|
||||
- [ ] Clear error messages (planned)
|
||||
- [ ] Progress indicators (planned)
|
||||
- [ ] Recovery options (planned)
|
||||
13
ios/.gitignore
vendored
@@ -11,3 +11,16 @@ capacitor-cordova-ios-plugins
|
||||
# Generated Config files
|
||||
App/App/capacitor.config.json
|
||||
App/App/config.xml
|
||||
|
||||
# User-specific Xcode files
|
||||
App/App.xcodeproj/xcuserdata/*.xcuserdatad/
|
||||
App/App.xcodeproj/*.xcuserstate
|
||||
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/screenshots
|
||||
fastlane/test_output
|
||||
|
||||
# Generated Icons from capacitor-assets (also Contents.json which is confusing; see BUILDING.md)
|
||||
App/App/Assets.xcassets/AppIcon.appiconset
|
||||
App/App/Assets.xcassets/Splash.imageset
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };
|
||||
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
|
||||
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
|
||||
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 */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
@@ -27,9 +27,9 @@
|
||||
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
|
||||
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
|
||||
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
90DCAFB4D8948F7A50C13800 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
|
||||
EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -37,17 +37,17 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */,
|
||||
97EF2DC6FD76C3643D680B8D /* Pods_App.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = {
|
||||
4B546315E668C7A13939F417 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */,
|
||||
90DCAFB4D8948F7A50C13800 /* Pods_App.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
@@ -57,8 +57,8 @@
|
||||
children = (
|
||||
504EC3061FED79650016851F /* App */,
|
||||
504EC3051FED79650016851F /* Products */,
|
||||
7F8756D8B27F46E3366F6CEA /* Pods */,
|
||||
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
|
||||
BA325FFCDCE8D334E5C7AEBE /* Pods */,
|
||||
4B546315E668C7A13939F417 /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -85,13 +85,13 @@
|
||||
path = App;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7F8756D8B27F46E3366F6CEA /* Pods */ = {
|
||||
BA325FFCDCE8D334E5C7AEBE /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */,
|
||||
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */,
|
||||
EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */,
|
||||
E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
@@ -101,12 +101,13 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */;
|
||||
buildPhases = (
|
||||
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */,
|
||||
92977BEA1068CC097A57FC77 /* [CP] Check Pods Manifest.lock */,
|
||||
504EC3001FED79650016851F /* Sources */,
|
||||
504EC3011FED79650016851F /* Frameworks */,
|
||||
504EC3021FED79650016851F /* Resources */,
|
||||
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */,
|
||||
012076E8FFE4BF260A79B034 /* Fix Privacy Manifest */,
|
||||
3525031ED1C96EF4CF6E9959 /* [CP] Embed Pods Frameworks */,
|
||||
96A7EF592DF3366D00084D51 /* Fix Privacy Manifest */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -186,28 +187,10 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PROJECT_DIR}/app_privacy_manifest_fixer/fixer.sh\" ";
|
||||
shellScript = "\"${PROJECT_DIR}/app_privacy_manifest_fixer/fixer.sh\" \n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-App-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */ = {
|
||||
3525031ED1C96EF4CF6E9959 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
@@ -222,6 +205,47 @@
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
92977BEA1068CC097A57FC77 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-App-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
96A7EF592DF3366D00084D51 /* Fix Privacy Manifest */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Fix Privacy Manifest";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "$PROJECT_DIR/app_privacy_manifest_fixer/fixer.sh\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@@ -375,11 +399,12 @@
|
||||
};
|
||||
504EC3171FED79650016851F /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */;
|
||||
baseConfigurationReference = EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 18;
|
||||
CURRENT_PROJECT_VERSION = 34;
|
||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||
ENABLE_APP_SANDBOX = NO;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
@@ -388,7 +413,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.4.7;
|
||||
MARKETING_VERSION = 0.5.8;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -401,11 +426,12 @@
|
||||
};
|
||||
504EC3181FED79650016851F /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */;
|
||||
baseConfigurationReference = E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 18;
|
||||
CURRENT_PROJECT_VERSION = 34;
|
||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||
ENABLE_APP_SANDBOX = NO;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
@@ -414,7 +440,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.4.7;
|
||||
MARKETING_VERSION = 0.5.8;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import UIKit
|
||||
import Capacitor
|
||||
import CapacitorCommunitySqlite
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
@@ -7,6 +8,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
var window: UIWindow?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
// Initialize SQLite
|
||||
//let sqlite = SQLite()
|
||||
//sqlite.initialize()
|
||||
|
||||
// Override point for customization after application launch.
|
||||
return true
|
||||
}
|
||||
|
||||
|
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>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>app.timesafari</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>timesafari</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -11,6 +11,7 @@ install! 'cocoapods', :disable_input_output_paths => true
|
||||
def capacitor_pods
|
||||
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
||||
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
||||
pod 'CapacitorCommunitySqlite', :path => '../../node_modules/@capacitor-community/sqlite'
|
||||
pod 'CapacitorMlkitBarcodeScanning', :path => '../../node_modules/@capacitor-mlkit/barcode-scanning'
|
||||
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
||||
pod 'CapacitorCamera', :path => '../../node_modules/@capacitor/camera'
|
||||
@@ -26,4 +27,9 @@ end
|
||||
|
||||
post_install do |installer|
|
||||
assertDeploymentTarget(installer)
|
||||
installer.pods_project.targets.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,6 +5,10 @@ PODS:
|
||||
- Capacitor
|
||||
- CapacitorCamera (6.1.2):
|
||||
- Capacitor
|
||||
- CapacitorCommunitySqlite (6.0.2):
|
||||
- Capacitor
|
||||
- SQLCipher
|
||||
- ZIPFoundation
|
||||
- CapacitorCordova (6.2.1)
|
||||
- CapacitorFilesystem (6.0.3):
|
||||
- Capacitor
|
||||
@@ -73,11 +77,18 @@ PODS:
|
||||
- nanopb/decode (2.30910.0)
|
||||
- nanopb/encode (2.30910.0)
|
||||
- PromisesObjC (2.4.0)
|
||||
- SQLCipher (4.9.0):
|
||||
- SQLCipher/standard (= 4.9.0)
|
||||
- SQLCipher/common (4.9.0)
|
||||
- SQLCipher/standard (4.9.0):
|
||||
- SQLCipher/common
|
||||
- ZIPFoundation (0.9.19)
|
||||
|
||||
DEPENDENCIES:
|
||||
- "Capacitor (from `../../node_modules/@capacitor/ios`)"
|
||||
- "CapacitorApp (from `../../node_modules/@capacitor/app`)"
|
||||
- "CapacitorCamera (from `../../node_modules/@capacitor/camera`)"
|
||||
- "CapacitorCommunitySqlite (from `../../node_modules/@capacitor-community/sqlite`)"
|
||||
- "CapacitorCordova (from `../../node_modules/@capacitor/ios`)"
|
||||
- "CapacitorFilesystem (from `../../node_modules/@capacitor/filesystem`)"
|
||||
- "CapacitorMlkitBarcodeScanning (from `../../node_modules/@capacitor-mlkit/barcode-scanning`)"
|
||||
@@ -98,6 +109,8 @@ SPEC REPOS:
|
||||
- MLKitVision
|
||||
- nanopb
|
||||
- PromisesObjC
|
||||
- SQLCipher
|
||||
- ZIPFoundation
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
Capacitor:
|
||||
@@ -106,6 +119,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../../node_modules/@capacitor/app"
|
||||
CapacitorCamera:
|
||||
:path: "../../node_modules/@capacitor/camera"
|
||||
CapacitorCommunitySqlite:
|
||||
:path: "../../node_modules/@capacitor-community/sqlite"
|
||||
CapacitorCordova:
|
||||
:path: "../../node_modules/@capacitor/ios"
|
||||
CapacitorFilesystem:
|
||||
@@ -121,6 +136,7 @@ SPEC CHECKSUMS:
|
||||
Capacitor: c95400d761e376be9da6be5a05f226c0e865cebf
|
||||
CapacitorApp: e1e6b7d05e444d593ca16fd6d76f2b7c48b5aea7
|
||||
CapacitorCamera: 9bc7b005d0e6f1d5f525b8137045b60cffffce79
|
||||
CapacitorCommunitySqlite: 0299d20f4b00c2e6aa485a1d8932656753937b9b
|
||||
CapacitorCordova: 8d93e14982f440181be7304aa9559ca631d77fff
|
||||
CapacitorFilesystem: 59270a63c60836248812671aa3b15df673fbaf74
|
||||
CapacitorMlkitBarcodeScanning: 7652be9c7922f39203a361de735d340ae37e134e
|
||||
@@ -138,7 +154,9 @@ SPEC CHECKSUMS:
|
||||
MLKitVision: 90922bca854014a856f8b649d1f1f04f63fd9c79
|
||||
nanopb: 438bc412db1928dac798aa6fd75726007be04262
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
SQLCipher: 31878d8ebd27e5c96db0b7cb695c96e9f8ad77da
|
||||
ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c
|
||||
|
||||
PODFILE CHECKSUM: 7e7e09e6937de7f015393aecf2cf7823645689b3
|
||||
PODFILE CHECKSUM: f987510f7383b04a1b09ea8472bdadcd88b6c924
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
4484
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "timesafari",
|
||||
"version": "0.4.6",
|
||||
"version": "0.5.8",
|
||||
"description": "Time Safari Application",
|
||||
"author": {
|
||||
"name": "Time Safari Team"
|
||||
@@ -46,6 +46,7 @@
|
||||
"electron:build-mac-universal": "npm run build:electron-prod && electron-builder --mac --universal"
|
||||
},
|
||||
"dependencies": {
|
||||
"@capacitor-community/sqlite": "6.0.2",
|
||||
"@capacitor-mlkit/barcode-scanning": "^6.0.0",
|
||||
"@capacitor/android": "^6.2.0",
|
||||
"@capacitor/app": "^6.0.0",
|
||||
@@ -166,11 +167,11 @@
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "~5.2.2",
|
||||
"vite": "^5.2.0",
|
||||
"vite-plugin-pwa": "^0.19.8"
|
||||
"vite-plugin-pwa": "^1.0.0"
|
||||
},
|
||||
"main": "./dist-electron/main.js",
|
||||
"build": {
|
||||
"appId": "app.timesafari",
|
||||
"appId": "app.timesafari.app",
|
||||
"productName": "TimeSafari",
|
||||
"directories": {
|
||||
"output": "dist-electron-packages"
|
||||
@@ -181,7 +182,7 @@
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "dist",
|
||||
"from": "dist-electron/www",
|
||||
"to": "www"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -2,5 +2,6 @@ dependencies:
|
||||
- gradle
|
||||
- java
|
||||
- pod
|
||||
- rubygems.org
|
||||
|
||||
# other dependencies are discovered via package.json & requirements.txt & Gemfile (I'm guessing).
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
eth_keys
|
||||
pywebview
|
||||
pyinstaller>=6.12.0
|
||||
setuptools>=69.0.0 # Required for distutils for electron-builder on macOS
|
||||
# For development
|
||||
watchdog>=3.0.0 # For file watching support
|
||||
@@ -1,10 +1,9 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
console.log('Starting electron build process...');
|
||||
console.log('Starting electron build process...');
|
||||
|
||||
// Copy web files
|
||||
const webDistPath = path.join(__dirname, '..', 'dist');
|
||||
// Define paths
|
||||
const electronDistPath = path.join(__dirname, '..', 'dist-electron');
|
||||
const wwwPath = path.join(electronDistPath, 'www');
|
||||
|
||||
@@ -13,231 +12,154 @@ if (!fs.existsSync(wwwPath)) {
|
||||
fs.mkdirSync(wwwPath, { recursive: true });
|
||||
}
|
||||
|
||||
// Copy web files to www directory
|
||||
fs.cpSync(webDistPath, wwwPath, { recursive: true });
|
||||
// Create a platform-specific index.html for Electron
|
||||
const initialIndexContent = `<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0,viewport-fit=cover">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<title>TimeSafari</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but TimeSafari doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<script type="module">
|
||||
// Force electron platform
|
||||
window.process = { env: { VITE_PLATFORM: 'electron' } };
|
||||
import('./src/main.electron.ts');
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
// 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');
|
||||
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
|
||||
indexContent = indexContent
|
||||
console.log('Fixing asset paths in index.html...');
|
||||
let modifiedIndexContent = fs.readFileSync(indexPath, 'utf8');
|
||||
modifiedIndexContent = modifiedIndexContent
|
||||
.replace(/\/assets\//g, './assets/')
|
||||
.replace(/href="\//g, 'href="./')
|
||||
.replace(/src="\//g, 'src="./');
|
||||
|
||||
fs.writeFileSync(indexPath, 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
|
||||
console.log('After path fixing, checking for remaining /assets/ paths:', indexContent.includes('/assets/'));
|
||||
console.log('Sample of fixed content:', indexContent.substring(0, 500));
|
||||
console.log('After path fixing, checking for remaining /assets/ paths:', finalContent.includes('/assets/'));
|
||||
console.log('Sample of fixed content:', finalContent.substring(0, 500));
|
||||
|
||||
console.log('Copied and fixed web files in:', wwwPath);
|
||||
|
||||
// Copy main process files
|
||||
console.log('Copying main process files...');
|
||||
// Copy main process files
|
||||
console.log('Copying main process files...');
|
||||
|
||||
// Create the main process file with inlined logger
|
||||
const mainContent = `const { app, BrowserWindow } = require("electron");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
// Copy the main process file instead of creating a template
|
||||
const mainSrcPath = path.join(__dirname, '..', 'dist-electron', 'main.js');
|
||||
const mainDestPath = path.join(electronDistPath, 'main.js');
|
||||
|
||||
// Inline logger implementation
|
||||
const logger = {
|
||||
log: (...args) => console.log(...args),
|
||||
error: (...args) => console.error(...args),
|
||||
info: (...args) => console.info(...args),
|
||||
warn: (...args) => console.warn(...args),
|
||||
debug: (...args) => console.debug(...args),
|
||||
};
|
||||
|
||||
// Check if running in dev mode
|
||||
const isDev = process.argv.includes("--inspect");
|
||||
|
||||
function createWindow() {
|
||||
// Add before createWindow function
|
||||
const preloadPath = path.join(__dirname, "preload.js");
|
||||
logger.log("Checking preload path:", preloadPath);
|
||||
logger.log("Preload exists:", fs.existsSync(preloadPath));
|
||||
|
||||
// Create the browser window.
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
webSecurity: true,
|
||||
allowRunningInsecureContent: false,
|
||||
preload: path.join(__dirname, "preload.js"),
|
||||
},
|
||||
});
|
||||
|
||||
// Always open DevTools for now
|
||||
mainWindow.webContents.openDevTools();
|
||||
|
||||
// Intercept requests to fix asset paths
|
||||
mainWindow.webContents.session.webRequest.onBeforeRequest(
|
||||
{
|
||||
urls: [
|
||||
"file://*/*/assets/*",
|
||||
"file://*/assets/*",
|
||||
"file:///assets/*", // Catch absolute paths
|
||||
"<all_urls>", // Catch all URLs as a fallback
|
||||
],
|
||||
},
|
||||
(details, callback) => {
|
||||
let url = details.url;
|
||||
|
||||
// Handle paths that don't start with file://
|
||||
if (!url.startsWith("file://") && url.includes("/assets/")) {
|
||||
url = \`file://\${path.join(__dirname, "www", url)}\`;
|
||||
}
|
||||
|
||||
// Handle absolute paths starting with /assets/
|
||||
if (url.includes("/assets/") && !url.includes("/www/assets/")) {
|
||||
const baseDir = url.includes("dist-electron")
|
||||
? url.substring(
|
||||
0,
|
||||
url.indexOf("/dist-electron") + "/dist-electron".length,
|
||||
)
|
||||
: \`file://\${__dirname}\`;
|
||||
const assetPath = url.split("/assets/")[1];
|
||||
const newUrl = \`\${baseDir}/www/assets/\${assetPath}\`;
|
||||
callback({ redirectURL: newUrl });
|
||||
return;
|
||||
}
|
||||
|
||||
callback({}); // No redirect for other URLs
|
||||
},
|
||||
);
|
||||
|
||||
if (isDev) {
|
||||
// Debug info
|
||||
logger.log("Debug Info:");
|
||||
logger.log("Running in dev mode:", isDev);
|
||||
logger.log("App is packaged:", app.isPackaged);
|
||||
logger.log("Process resource path:", process.resourcesPath);
|
||||
logger.log("App path:", app.getAppPath());
|
||||
logger.log("__dirname:", __dirname);
|
||||
logger.log("process.cwd():", process.cwd());
|
||||
}
|
||||
|
||||
const indexPath = path.join(__dirname, "www", "index.html");
|
||||
|
||||
if (isDev) {
|
||||
logger.log("Loading index from:", indexPath);
|
||||
logger.log("www path:", path.join(__dirname, "www"));
|
||||
logger.log("www assets path:", path.join(__dirname, "www", "assets"));
|
||||
}
|
||||
|
||||
if (!fs.existsSync(indexPath)) {
|
||||
logger.error(\`Index file not found at: \${indexPath}\`);
|
||||
throw new Error("Index file not found");
|
||||
}
|
||||
|
||||
// Add CSP headers to allow API connections, Google Fonts, and zxing-wasm
|
||||
mainWindow.webContents.session.webRequest.onHeadersReceived(
|
||||
(details, callback) => {
|
||||
callback({
|
||||
responseHeaders: {
|
||||
...details.responseHeaders,
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'self';" +
|
||||
"connect-src 'self' https://api.endorser.ch https://*.timesafari.app https://*.jsdelivr.net;" +
|
||||
"img-src 'self' data: https: blob:;" +
|
||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://*.jsdelivr.net;" +
|
||||
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;" +
|
||||
"font-src 'self' data: https://fonts.gstatic.com;" +
|
||||
"style-src-elem 'self' 'unsafe-inline' https://fonts.googleapis.com;" +
|
||||
"worker-src 'self' blob:;",
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Load the index.html
|
||||
mainWindow
|
||||
.loadFile(indexPath)
|
||||
.then(() => {
|
||||
logger.log("Successfully loaded index.html");
|
||||
if (isDev) {
|
||||
mainWindow.webContents.openDevTools();
|
||||
logger.log("DevTools opened - running in dev mode");
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.error("Failed to load index.html:", err);
|
||||
logger.error("Attempted path:", indexPath);
|
||||
});
|
||||
|
||||
// Listen for console messages from the renderer
|
||||
mainWindow.webContents.on("console-message", (_event, _level, message) => {
|
||||
logger.log("Renderer Console:", message);
|
||||
});
|
||||
|
||||
// Add right after creating the BrowserWindow
|
||||
mainWindow.webContents.on(
|
||||
"did-fail-load",
|
||||
(_event, errorCode, errorDescription) => {
|
||||
logger.error("Page failed to load:", errorCode, errorDescription);
|
||||
},
|
||||
);
|
||||
|
||||
mainWindow.webContents.on("preload-error", (_event, preloadPath, error) => {
|
||||
logger.error("Preload script error:", preloadPath, error);
|
||||
});
|
||||
|
||||
mainWindow.webContents.on(
|
||||
"console-message",
|
||||
(_event, _level, message, line, sourceId) => {
|
||||
logger.log("Renderer Console:", line, sourceId, message);
|
||||
},
|
||||
);
|
||||
|
||||
// Enable remote debugging when in dev mode
|
||||
if (isDev) {
|
||||
mainWindow.webContents.openDevTools();
|
||||
}
|
||||
if (fs.existsSync(mainSrcPath)) {
|
||||
fs.copyFileSync(mainSrcPath, mainDestPath);
|
||||
console.log('Copied main process file successfully');
|
||||
} else {
|
||||
console.error('Main process file not found at:', mainSrcPath);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Handle app ready
|
||||
app.whenReady().then(createWindow);
|
||||
|
||||
// Handle all windows closed
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on("activate", () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle any errors
|
||||
process.on("uncaughtException", (error) => {
|
||||
logger.error("Uncaught Exception:", error);
|
||||
});
|
||||
`;
|
||||
|
||||
// Write the main process file
|
||||
const mainDest = path.join(electronDistPath, 'main.js');
|
||||
fs.writeFileSync(mainDest, mainContent);
|
||||
|
||||
// Copy preload script if it exists
|
||||
const preloadSrc = path.join(__dirname, '..', 'src', 'electron', 'preload.js');
|
||||
const preloadDest = path.join(electronDistPath, 'preload.js');
|
||||
if (fs.existsSync(preloadSrc)) {
|
||||
console.log(`Copying ${preloadSrc} to ${preloadDest}`);
|
||||
fs.copyFileSync(preloadSrc, preloadDest);
|
||||
}
|
||||
|
||||
// Verify build structure
|
||||
console.log('\nVerifying build structure:');
|
||||
console.log('Files in dist-electron:', fs.readdirSync(electronDistPath));
|
||||
|
||||
console.log('Build completed successfully!');
|
||||
console.log('Electron build process completed successfully');
|
||||
@@ -51,7 +51,7 @@ const { existsSync } = require('fs');
|
||||
*/
|
||||
function checkCommand(command, errorMessage) {
|
||||
try {
|
||||
execSync(command + ' --version', { stdio: 'ignore' });
|
||||
execSync(command, { stdio: 'ignore' });
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error(`❌ ${errorMessage}`);
|
||||
@@ -164,10 +164,10 @@ function main() {
|
||||
|
||||
// Check required command line tools
|
||||
// These are essential for building and testing the application
|
||||
success &= checkCommand('node', 'Node.js is required');
|
||||
success &= checkCommand('npm', 'npm is required');
|
||||
success &= checkCommand('gradle', 'Gradle is required for Android builds');
|
||||
success &= checkCommand('xcodebuild', 'Xcode is required for iOS builds');
|
||||
success &= checkCommand('node --version', 'Node.js is required');
|
||||
success &= checkCommand('npm --version', 'npm is required');
|
||||
success &= checkCommand('gradle --version', 'Gradle is required for Android builds');
|
||||
success &= checkCommand('xcodebuild --help', 'Xcode is required for iOS builds');
|
||||
|
||||
// Check platform-specific development environments
|
||||
success &= checkAndroidSetup();
|
||||
|
||||
@@ -170,7 +170,7 @@ const executeDeeplink = async (url, description, log) => {
|
||||
|
||||
try {
|
||||
// Stop the app before executing the deep link
|
||||
execSync('adb shell am force-stop app.timesafari');
|
||||
execSync('adb shell am force-stop app.timesafari.app');
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1s
|
||||
|
||||
execSync(`adb shell am start -W -a android.intent.action.VIEW -d "${url}" -c android.intent.category.BROWSABLE`);
|
||||
|
||||
25
src/App.vue
@@ -4,7 +4,7 @@
|
||||
<!-- Messages in the upper-right - https://github.com/emmanuelsw/notiwind -->
|
||||
<NotificationGroup group="alert">
|
||||
<div
|
||||
class="fixed 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
|
||||
v-slot="{ notifications, close }"
|
||||
@@ -330,8 +330,11 @@
|
||||
|
||||
<script lang="ts">
|
||||
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";
|
||||
|
||||
interface Settings {
|
||||
@@ -396,7 +399,11 @@ export default class App extends Vue {
|
||||
|
||||
try {
|
||||
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);
|
||||
|
||||
const notifyingNewActivity = !!settings?.notifyingNewActivityTime;
|
||||
@@ -541,13 +548,13 @@ export default class App extends Vue {
|
||||
|
||||
<style>
|
||||
#Content {
|
||||
padding-left: 1.5rem;
|
||||
padding-right: 1.5rem;
|
||||
padding-top: calc(env(safe-area-inset-top) + 1.5rem);
|
||||
padding-bottom: calc(env(safe-area-inset-bottom) + 1.5rem);
|
||||
padding-left: max(1.5rem, env(safe-area-inset-left));
|
||||
padding-right: max(1.5rem, env(safe-area-inset-right));
|
||||
padding-top: max(1.5rem, env(safe-area-inset-top));
|
||||
padding-bottom: max(1.5rem, env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
#QuickNav ~ #Content {
|
||||
padding-bottom: calc(env(safe-area-inset-bottom) + 6rem);
|
||||
padding-bottom: calc(env(safe-area-inset-bottom) + 6.333rem);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<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
|
||||
:entity-id="record.issuerDid"
|
||||
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<font-awesome
|
||||
icon="person-circle-question"
|
||||
class="text-slate-300 text-[2rem]"
|
||||
/>
|
||||
</div>
|
||||
</router-link>
|
||||
<font-awesome
|
||||
v-else-if="isHiddenDid(record.issuerDid)"
|
||||
icon="eye-slash"
|
||||
class="text-slate-400 !size-[2rem] cursor-pointer"
|
||||
@click="notifyHiddenPerson"
|
||||
/>
|
||||
<font-awesome
|
||||
v-else
|
||||
icon="person-circle-question"
|
||||
class="text-slate-400 !size-[2rem] cursor-pointer"
|
||||
@click="notifyUnknownPerson"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<h3 class="font-semibold">
|
||||
{{ record.issuer.known ? record.issuer.displayName : "" }}
|
||||
<h3 v-if="record.issuer.known" class="font-semibold leading-tight">
|
||||
{{ record.issuer.displayName }}
|
||||
</h3>
|
||||
<p class="ms-auto text-xs text-slate-500 italic">
|
||||
{{ friendlyDate }}
|
||||
@@ -37,7 +49,11 @@
|
||||
</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" />
|
||||
</a>
|
||||
</div>
|
||||
@@ -46,7 +62,7 @@
|
||||
<!-- Record Image -->
|
||||
<div
|
||||
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});`"
|
||||
>
|
||||
<a
|
||||
@@ -62,29 +78,59 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<p class="font-medium">
|
||||
<a class="cursor-pointer" @click="$emit('loadClaim', record.jwtId)">
|
||||
{{ description }}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="relative flex justify-between gap-4 max-w-[40rem] mx-auto mb-5"
|
||||
class="relative flex justify-between gap-4 max-w-[40rem] mx-auto mt-4"
|
||||
>
|
||||
<!-- Source -->
|
||||
<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>
|
||||
<!-- Project Icon -->
|
||||
<div v-if="record.providerPlanName">
|
||||
<ProjectIcon
|
||||
:entity-id="record.providerPlanName"
|
||||
:icon-size="48"
|
||||
class="rounded size-[3rem] sm:size-[4rem] *:w-full *:h-full"
|
||||
/>
|
||||
<router-link
|
||||
:to="{
|
||||
path:
|
||||
'/project/' +
|
||||
encodeURIComponent(record.providerPlanHandleId || ''),
|
||||
}"
|
||||
title="View project details"
|
||||
>
|
||||
<ProjectIcon
|
||||
:entity-id="record.providerPlanHandleId || ''"
|
||||
:icon-size="48"
|
||||
class="rounded size-[3rem] sm:size-[4rem] *:w-full *:h-full"
|
||||
/>
|
||||
</router-link>
|
||||
</div>
|
||||
<!-- Identicon for DIDs -->
|
||||
<div v-else-if="record.agentDid">
|
||||
<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
|
||||
v-if="!isHiddenDid(record.agentDid)"
|
||||
:to="{
|
||||
path: '/did/' + encodeURIComponent(record.agentDid),
|
||||
}"
|
||||
title="More details about this person"
|
||||
>
|
||||
<EntityIcon
|
||||
:entity-id="record.agentDid"
|
||||
:profile-image-url="record.issuer.profileImageUrl"
|
||||
class="rounded-full bg-slate-100 overflow-hidden !size-[3rem] sm:!size-[4rem]"
|
||||
/>
|
||||
</router-link>
|
||||
<font-awesome
|
||||
v-else
|
||||
icon="eye-slash"
|
||||
class="text-slate-300 !size-[3rem] sm:!size-[4rem]"
|
||||
@click="notifyHiddenPerson"
|
||||
/>
|
||||
</div>
|
||||
<!-- Unknown Person -->
|
||||
@@ -92,6 +138,7 @@
|
||||
<font-awesome
|
||||
icon="person-circle-question"
|
||||
class="text-slate-300 text-[3rem] sm:text-[4rem]"
|
||||
@click="notifyUnknownPerson"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -110,9 +157,11 @@
|
||||
|
||||
<!-- Arrow -->
|
||||
<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 }}
|
||||
</div>
|
||||
|
||||
@@ -129,24 +178,47 @@
|
||||
|
||||
<!-- Destination -->
|
||||
<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>
|
||||
<!-- Project Icon -->
|
||||
<div v-if="record.recipientProjectName">
|
||||
<ProjectIcon
|
||||
:entity-id="record.recipientProjectName"
|
||||
:icon-size="48"
|
||||
class="rounded size-[3rem] sm:size-[4rem] *:w-full *:h-full"
|
||||
/>
|
||||
<router-link
|
||||
:to="{
|
||||
path:
|
||||
'/project/' +
|
||||
encodeURIComponent(record.fulfillsPlanHandleId || ''),
|
||||
}"
|
||||
title="View project details"
|
||||
>
|
||||
<ProjectIcon
|
||||
:entity-id="record.fulfillsPlanHandleId || ''"
|
||||
:icon-size="48"
|
||||
class="rounded size-[3rem] sm:size-[4rem] *:w-full *:h-full"
|
||||
/>
|
||||
</router-link>
|
||||
</div>
|
||||
<!-- Identicon for DIDs -->
|
||||
<div v-else-if="record.recipientDid">
|
||||
<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
|
||||
v-if="!isHiddenDid(record.recipientDid)"
|
||||
:to="{
|
||||
path: '/did/' + encodeURIComponent(record.recipientDid),
|
||||
}"
|
||||
title="More details about this person"
|
||||
>
|
||||
<EntityIcon
|
||||
:entity-id="record.recipientDid"
|
||||
:profile-image-url="record.receiver.profileImageUrl"
|
||||
class="rounded-full bg-slate-100 overflow-hidden !size-[3rem] sm:!size-[4rem]"
|
||||
/>
|
||||
</router-link>
|
||||
<font-awesome
|
||||
v-else
|
||||
icon="eye-slash"
|
||||
class="text-slate-300 !size-[3rem] sm:!size-[4rem]"
|
||||
@click="notifyHiddenPerson"
|
||||
/>
|
||||
</div>
|
||||
<!-- Unknown Person -->
|
||||
@@ -154,6 +226,7 @@
|
||||
<font-awesome
|
||||
icon="person-circle-question"
|
||||
class="text-slate-300 text-[3rem] sm:text-[4rem]"
|
||||
@click="notifyUnknownPerson"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -170,13 +243,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<p class="font-medium">
|
||||
<a class="cursor-pointer" @click="$emit('loadClaim', record.jwtId)">
|
||||
{{ description }}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
@@ -186,8 +252,9 @@ import { Component, Prop, Vue, Emit } from "vue-facing-decorator";
|
||||
import { GiveRecordWithContactInfo } from "../types";
|
||||
import EntityIcon from "./EntityIcon.vue";
|
||||
import { isGiveClaimType, notifyWhyCannotConfirm } from "../libs/util";
|
||||
import { containsHiddenDid } from "../libs/endorserServer";
|
||||
import { containsHiddenDid, isHiddenDid } from "../libs/endorserServer";
|
||||
import ProjectIcon from "./ProjectIcon.vue";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
@@ -202,6 +269,33 @@ export default class ActivityListItem extends Vue {
|
||||
@Prop() activeDid!: string;
|
||||
@Prop() confirmerIdList?: string[];
|
||||
|
||||
isHiddenDid = isHiddenDid;
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
|
||||
notifyHiddenPerson() {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "warning",
|
||||
title: "Person Outside Your Network",
|
||||
text: "This person is not visible to you.",
|
||||
},
|
||||
3000,
|
||||
);
|
||||
}
|
||||
|
||||
notifyUnknownPerson() {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "warning",
|
||||
title: "Unidentified Person",
|
||||
text: "Nobody specific was recognized.",
|
||||
},
|
||||
3000,
|
||||
);
|
||||
}
|
||||
|
||||
@Emit()
|
||||
cacheImage(image: string) {
|
||||
return image;
|
||||
@@ -222,7 +316,7 @@ export default class ActivityListItem extends Vue {
|
||||
const claim =
|
||||
(this.record.fullClaim as unknown).claim || this.record.fullClaim;
|
||||
|
||||
return `${claim.description}`;
|
||||
return `${claim?.description || ""}`;
|
||||
}
|
||||
|
||||
private displayAmount(code: string, amt: number) {
|
||||
|
||||
@@ -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"
|
||||
@click="exportDatabase()"
|
||||
>
|
||||
Download Settings & Contacts
|
||||
<br />
|
||||
(excluding Identifier Data)
|
||||
Download Contacts
|
||||
</button>
|
||||
<a
|
||||
ref="downloadLink"
|
||||
@@ -62,14 +60,18 @@ backup and database export, with platform-specific download instructions. * *
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-facing-decorator";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import { db } from "../db/index";
|
||||
import { AppString, NotificationIface } from "../constants/app";
|
||||
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
||||
import {
|
||||
PlatformService,
|
||||
PlatformCapabilities,
|
||||
} from "../services/PlatformService";
|
||||
import { contactsToExportJson } from "../libs/util";
|
||||
|
||||
/**
|
||||
* @vue-component
|
||||
@@ -131,21 +133,25 @@ export default class DataExportSection extends Vue {
|
||||
*/
|
||||
public async exportDatabase() {
|
||||
try {
|
||||
const blob = await db.export({
|
||||
prettyJson: true,
|
||||
transform: (table, value, key) => {
|
||||
if (table === "contacts") {
|
||||
// Dexie inserts a number 0 when some are undefined, so we need to totally remove them.
|
||||
Object.keys(value).forEach((prop) => {
|
||||
if (value[prop] === undefined) {
|
||||
delete value[prop];
|
||||
}
|
||||
});
|
||||
}
|
||||
return { value, key };
|
||||
},
|
||||
});
|
||||
const fileName = `${db.name}-backup.json`;
|
||||
let allContacts: Contact[] = [];
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const result = await platformService.dbQuery(`SELECT * FROM contacts`);
|
||||
if (result) {
|
||||
allContacts = databaseUtil.mapQueryResultToValues(
|
||||
result,
|
||||
) as unknown as Contact[];
|
||||
}
|
||||
// if (USE_DEXIE_DB) {
|
||||
// await db.open();
|
||||
// allContacts = await db.contacts.toArray();
|
||||
// }
|
||||
|
||||
// Convert contacts to export format
|
||||
const exportData = contactsToExportJson(allContacts);
|
||||
const jsonStr = JSON.stringify(exportData, null, 2);
|
||||
const blob = new Blob([jsonStr], { type: "application/json" });
|
||||
|
||||
const fileName = `${AppString.APP_NAME_NO_SPACES}-backup-contacts.json`;
|
||||
|
||||
if (this.platformCapabilities.hasFileDownload) {
|
||||
// Web platform: Use download link
|
||||
@@ -157,8 +163,9 @@ export default class DataExportSection extends Vue {
|
||||
setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000);
|
||||
} else if (this.platformCapabilities.hasFileSystem) {
|
||||
// Native platform: Write to app directory
|
||||
const content = await blob.text();
|
||||
await this.platformService.writeAndShareFile(fileName, content);
|
||||
await this.platformService.writeAndShareFile(fileName, jsonStr);
|
||||
} else {
|
||||
throw new Error("This platform does not support file downloads.");
|
||||
}
|
||||
|
||||
this.$notify(
|
||||
@@ -167,10 +174,10 @@ export default class DataExportSection extends Vue {
|
||||
type: "success",
|
||||
title: "Export Successful",
|
||||
text: this.platformCapabilities.hasFileDownload
|
||||
? "See your downloads directory for the backup. It is in the Dexie format."
|
||||
: "You should have been prompted to save your backup file.",
|
||||
? "See your downloads directory for the backup."
|
||||
: "The backup file has been saved.",
|
||||
},
|
||||
-1,
|
||||
3000,
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error("Export Error:", error);
|
||||
|
||||
@@ -99,6 +99,9 @@ import {
|
||||
LTileLayer,
|
||||
} from "@vue-leaflet/vue-leaflet";
|
||||
import { Router } from "vue-router";
|
||||
|
||||
import { USE_DEXIE_DB } from "@/constants/app";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||
|
||||
@@ -122,7 +125,10 @@ export default class FeedFilters extends Vue {
|
||||
async open(onCloseIfChanged: () => void) {
|
||||
this.onCloseIfChanged = onCloseIfChanged;
|
||||
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
if (USE_DEXIE_DB) {
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
}
|
||||
this.hasVisibleDid = !!settings.filterFeedByVisible;
|
||||
this.isNearby = !!settings.filterFeedByNearby;
|
||||
if (settings.searchBoxes && settings.searchBoxes.length > 0) {
|
||||
@@ -136,17 +142,29 @@ export default class FeedFilters extends Vue {
|
||||
async toggleHasVisibleDid() {
|
||||
this.settingChanged = true;
|
||||
this.hasVisibleDid = !this.hasVisibleDid;
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
await databaseUtil.updateDefaultSettings({
|
||||
filterFeedByVisible: this.hasVisibleDid,
|
||||
});
|
||||
|
||||
if (USE_DEXIE_DB) {
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
filterFeedByVisible: this.hasVisibleDid,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async toggleNearby() {
|
||||
this.settingChanged = true;
|
||||
this.isNearby = !this.isNearby;
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
await databaseUtil.updateDefaultSettings({
|
||||
filterFeedByNearby: this.isNearby,
|
||||
});
|
||||
|
||||
if (USE_DEXIE_DB) {
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
filterFeedByNearby: this.isNearby,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async clearAll() {
|
||||
@@ -154,11 +172,18 @@ export default class FeedFilters extends Vue {
|
||||
this.settingChanged = true;
|
||||
}
|
||||
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
await databaseUtil.updateDefaultSettings({
|
||||
filterFeedByNearby: false,
|
||||
filterFeedByVisible: false,
|
||||
});
|
||||
|
||||
if (USE_DEXIE_DB) {
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
filterFeedByNearby: false,
|
||||
filterFeedByVisible: false,
|
||||
});
|
||||
}
|
||||
|
||||
this.hasVisibleDid = false;
|
||||
this.isNearby = false;
|
||||
}
|
||||
@@ -168,11 +193,18 @@ export default class FeedFilters extends Vue {
|
||||
this.settingChanged = true;
|
||||
}
|
||||
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
await databaseUtil.updateDefaultSettings({
|
||||
filterFeedByNearby: true,
|
||||
filterFeedByVisible: true,
|
||||
});
|
||||
|
||||
if (USE_DEXIE_DB) {
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
filterFeedByNearby: true,
|
||||
filterFeedByVisible: true,
|
||||
});
|
||||
}
|
||||
|
||||
this.hasVisibleDid = true;
|
||||
this.isNearby = true;
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
<script lang="ts">
|
||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||
import {
|
||||
createAndSubmitGive,
|
||||
didInfo,
|
||||
@@ -98,8 +98,10 @@ import {
|
||||
import * as libsUtil from "../libs/util";
|
||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
import { retrieveAccountDids } from "../libs/util";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
|
||||
@Component
|
||||
export default class GiftedDialog extends Vue {
|
||||
@@ -144,11 +146,23 @@ export default class GiftedDialog extends Vue {
|
||||
this.offerId = offerId || "";
|
||||
|
||||
try {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
if (USE_DEXIE_DB) {
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
}
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.activeDid = settings.activeDid || "";
|
||||
|
||||
this.allContacts = await db.contacts.toArray();
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const result = await platformService.dbQuery(`SELECT * FROM contacts`);
|
||||
if (result) {
|
||||
this.allContacts = databaseUtil.mapQueryResultToValues(
|
||||
result,
|
||||
) as unknown as Contact[];
|
||||
}
|
||||
if (USE_DEXIE_DB) {
|
||||
this.allContacts = await db.contacts.toArray();
|
||||
}
|
||||
|
||||
this.allMyDids = await retrieveAccountDids();
|
||||
|
||||
@@ -306,11 +320,8 @@ export default class GiftedDialog extends Vue {
|
||||
this.fromProjectId,
|
||||
);
|
||||
|
||||
if (
|
||||
result.type === "error" ||
|
||||
this.isGiveCreationError(result.response)
|
||||
) {
|
||||
const errorMessage = this.getGiveCreationErrorMessage(result);
|
||||
if (!result.success) {
|
||||
const errorMessage = result.error;
|
||||
logger.error("Error with give creation result:", result);
|
||||
this.$notify(
|
||||
{
|
||||
@@ -356,28 +367,6 @@ export default class GiftedDialog extends Vue {
|
||||
|
||||
// Helper functions for readability
|
||||
|
||||
/**
|
||||
* @param result response "data" from the server
|
||||
* @returns true if the result indicates an error
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
isGiveCreationError(result: any) {
|
||||
return result.status !== 201 || result.data?.error;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
|
||||
* @returns best guess at an error message
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getGiveCreationErrorMessage(result: any) {
|
||||
return (
|
||||
result.error?.userMessage ||
|
||||
result.error?.error ||
|
||||
result.response?.data?.error?.message
|
||||
);
|
||||
}
|
||||
|
||||
explainData() {
|
||||
this.$notify(
|
||||
{
|
||||
|
||||
@@ -74,10 +74,12 @@
|
||||
import { Vue, Component } from "vue-facing-decorator";
|
||||
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 { Contact } from "../db/tables/contacts";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
import { GiverReceiverInputInfo } from "../libs/util";
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
|
||||
@Component
|
||||
export default class GivenPrompts extends Vue {
|
||||
@@ -127,8 +129,16 @@ export default class GivenPrompts extends Vue {
|
||||
this.visible = true;
|
||||
this.callbackOnFullGiftInfo = callbackOnFullGiftInfo;
|
||||
|
||||
await db.open();
|
||||
this.numContacts = await db.contacts.count();
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const result = await platformService.dbQuery(
|
||||
"SELECT COUNT(*) FROM contacts",
|
||||
);
|
||||
if (result) {
|
||||
this.numContacts = result.values[0][0] as number;
|
||||
}
|
||||
if (USE_DEXIE_DB) {
|
||||
this.numContacts = await db.contacts.count();
|
||||
}
|
||||
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 count = 0;
|
||||
|
||||
// as long as the index has an entry, loop
|
||||
while (
|
||||
this.shownContactDbIndices[someContactDbIndex] != null &&
|
||||
@@ -229,10 +240,21 @@ export default class GivenPrompts extends Vue {
|
||||
this.nextIdeaPastContacts();
|
||||
} else {
|
||||
// get the contact at that offset
|
||||
await db.open();
|
||||
this.currentContact = await db.contacts
|
||||
.offset(someContactDbIndex)
|
||||
.first();
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const result = await platformService.dbQuery(
|
||||
"SELECT * FROM contacts LIMIT 1 OFFSET ?",
|
||||
[someContactDbIndex],
|
||||
);
|
||||
if (result) {
|
||||
const mappedContacts = databaseUtil.mapQueryResultToValues(result);
|
||||
this.currentContact = mappedContacts[0] as unknown as Contact;
|
||||
}
|
||||
if (USE_DEXIE_DB) {
|
||||
await db.open();
|
||||
this.currentContact = await db.contacts
|
||||
.offset(someContactDbIndex)
|
||||
.first();
|
||||
}
|
||||
this.shownContactDbIndices[someContactDbIndex] = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,16 +48,15 @@
|
||||
<span>
|
||||
{{ didInfo(visDid) }}
|
||||
<span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)">
|
||||
<a
|
||||
:href="`/did/${visDid}`"
|
||||
target="_blank"
|
||||
<router-link
|
||||
:to="{ path: '/did/' + encodeURIComponent(visDid) }"
|
||||
class="text-blue-500"
|
||||
>
|
||||
<font-awesome
|
||||
icon="arrow-up-right-from-square"
|
||||
class="fa-fw"
|
||||
/>
|
||||
</a>
|
||||
</router-link>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
@@ -78,7 +77,7 @@
|
||||
If you'd like an introduction,
|
||||
<a
|
||||
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
|
||||
they'll tell you more about the {{ roleName }}.</a
|
||||
>
|
||||
@@ -105,7 +104,7 @@ import * as R from "ramda";
|
||||
import { useClipboard } from "@vueuse/core";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import * as serverUtil from "../libs/endorserServer";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import { APP_SERVER, NotificationIface } from "../constants/app";
|
||||
|
||||
@Component
|
||||
export default class HiddenDidDialog extends Vue {
|
||||
@@ -118,7 +117,8 @@ export default class HiddenDidDialog extends Vue {
|
||||
activeDid = "";
|
||||
allMyDids: Array<string> = [];
|
||||
canShare = false;
|
||||
windowLocation = window.location.href;
|
||||
deepLinkPathSuffix = "";
|
||||
deepLinkUrl = window.location.href; // this is changed to a deep link in the setup
|
||||
|
||||
R = R;
|
||||
serverUtil = serverUtil;
|
||||
@@ -130,17 +130,21 @@ export default class HiddenDidDialog extends Vue {
|
||||
}
|
||||
|
||||
open(
|
||||
deepLinkPathSuffix: string,
|
||||
roleName: string,
|
||||
visibleToDids: string[],
|
||||
allContacts: Array<Contact>,
|
||||
activeDid: string,
|
||||
allMyDids: Array<string>,
|
||||
) {
|
||||
this.deepLinkPathSuffix = deepLinkPathSuffix;
|
||||
this.roleName = roleName;
|
||||
this.visibleToDids = visibleToDids;
|
||||
this.allContacts = allContacts;
|
||||
this.activeDid = activeDid;
|
||||
this.allMyDids = allMyDids;
|
||||
|
||||
this.deepLinkUrl = APP_SERVER + "/deep-link/" + this.deepLinkPathSuffix;
|
||||
this.isOpen = true;
|
||||
}
|
||||
|
||||
@@ -174,11 +178,11 @@ export default class HiddenDidDialog extends Vue {
|
||||
}
|
||||
|
||||
onClickShareClaim() {
|
||||
this.copyToClipboard("A link to this page", this.windowLocation);
|
||||
this.copyToClipboard("A link to this page", this.deepLinkUrl);
|
||||
window.navigator.share({
|
||||
title: "Help Connect Me",
|
||||
text: "I'm trying to find the people who recorded this. Can you help me?",
|
||||
url: this.windowLocation,
|
||||
url: this.deepLinkUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
<div class="text-lg text-center font-bold relative">
|
||||
<h1 id="ViewHeading" class="text-center font-bold">
|
||||
<span v-if="uploading">Uploading Image…</span>
|
||||
<span v-else-if="blob">Crop Image</span>
|
||||
<span v-else-if="blob">{{
|
||||
crop ? "Crop Image" : "Preview Image"
|
||||
}}</span>
|
||||
<span v-else-if="showCameraPreview">Upload Image</span>
|
||||
<span v-else>Add Photo</span>
|
||||
</h1>
|
||||
@@ -119,12 +121,23 @@
|
||||
playsinline
|
||||
muted
|
||||
></video>
|
||||
<button
|
||||
class="absolute bottom-4 left-1/2 -translate-x-1/2 bg-white text-slate-800 p-3 rounded-full text-2xl leading-none"
|
||||
@click="capturePhoto"
|
||||
<div
|
||||
class="absolute bottom-4 inset-x-0 flex items-center justify-center gap-4"
|
||||
>
|
||||
<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
|
||||
@@ -229,12 +242,12 @@
|
||||
<p class="mb-2">
|
||||
Before you can upload a photo, a friend needs to register you.
|
||||
</p>
|
||||
<router-link
|
||||
:to="{ name: 'contact-qr' }"
|
||||
<button
|
||||
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
|
||||
</router-link>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -247,11 +260,17 @@ import axios from "axios";
|
||||
import { ref } from "vue";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
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 { accessToken } from "../libs/crypto";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
|
||||
const inputImageFileNameRef = ref<Blob>();
|
||||
|
||||
@@ -262,6 +281,11 @@ const inputImageFileNameRef = ref<Blob>();
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
defaultCameraMode: {
|
||||
type: String,
|
||||
default: "environment",
|
||||
validator: (value: string) => ["environment", "user"].includes(value),
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class ImageMethodDialog extends Vue {
|
||||
@@ -303,6 +327,9 @@ export default class ImageMethodDialog extends Vue {
|
||||
/** Camera stream reference */
|
||||
private cameraStream: MediaStream | null = null;
|
||||
|
||||
/** Current camera facing mode */
|
||||
private currentFacingMode: "environment" | "user" = "environment";
|
||||
|
||||
private platformService = PlatformServiceFactory.getInstance();
|
||||
URL = window.URL || window.webkitURL;
|
||||
|
||||
@@ -334,7 +361,10 @@ export default class ImageMethodDialog extends Vue {
|
||||
*/
|
||||
async mounted() {
|
||||
try {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
if (USE_DEXIE_DB) {
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
}
|
||||
this.activeDid = settings.activeDid || "";
|
||||
} catch (error: unknown) {
|
||||
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) {
|
||||
logger.debug("ImageMethodDialog.open called");
|
||||
this.claimType = claimType;
|
||||
this.crop = !!crop;
|
||||
this.imageCallback = setImageFn;
|
||||
this.visible = true;
|
||||
this.currentFacingMode = this.defaultCameraMode as "environment" | "user";
|
||||
|
||||
// Start camera preview immediately if not on mobile
|
||||
if (!this.platformCapabilities.isNativeApp) {
|
||||
this.startCameraPreview();
|
||||
}
|
||||
// Start camera preview immediately
|
||||
logger.debug("Starting camera preview from open()");
|
||||
this.startCameraPreview();
|
||||
}
|
||||
|
||||
async uploadImageFile(event: Event) {
|
||||
@@ -438,46 +469,24 @@ export default class ImageMethodDialog extends Vue {
|
||||
logger.debug("startCameraPreview called");
|
||||
logger.debug("Current showCameraPreview state:", this.showCameraPreview);
|
||||
logger.debug("Platform capabilities:", this.platformCapabilities);
|
||||
logger.debug("MediaDevices available:", !!navigator.mediaDevices);
|
||||
logger.debug(
|
||||
"getUserMedia available:",
|
||||
!!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia),
|
||||
);
|
||||
|
||||
if (this.platformCapabilities.isNativeApp) {
|
||||
logger.debug("Using platform service for mobile device");
|
||||
this.cameraState = "initializing";
|
||||
this.cameraStateMessage = "Using platform camera service...";
|
||||
try {
|
||||
const result = await this.platformService.takePicture();
|
||||
this.blob = result.blob;
|
||||
this.fileName = result.fileName;
|
||||
this.cameraState = "ready";
|
||||
this.cameraStateMessage = "Photo captured successfully";
|
||||
} catch (error) {
|
||||
logger.error("Error taking picture:", error);
|
||||
this.cameraState = "error";
|
||||
this.cameraStateMessage =
|
||||
error instanceof Error ? error.message : "Failed to take picture";
|
||||
this.error =
|
||||
error instanceof Error ? error.message : "Failed to take picture";
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text: "Failed to take picture. Please try again.",
|
||||
},
|
||||
5000,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("Starting camera preview for desktop browser");
|
||||
try {
|
||||
this.cameraState = "initializing";
|
||||
this.cameraStateMessage = "Requesting camera access...";
|
||||
this.showCameraPreview = true;
|
||||
await this.$nextTick();
|
||||
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
||||
throw new Error("Camera API not available in this browser");
|
||||
}
|
||||
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { facingMode: "environment" },
|
||||
video: { facingMode: this.currentFacingMode },
|
||||
});
|
||||
logger.debug("Camera access granted");
|
||||
this.cameraStream = stream;
|
||||
@@ -491,25 +500,36 @@ export default class ImageMethodDialog extends Vue {
|
||||
videoElement.srcObject = stream;
|
||||
await new Promise((resolve) => {
|
||||
videoElement.onloadedmetadata = () => {
|
||||
videoElement.play().then(() => {
|
||||
resolve(true);
|
||||
});
|
||||
videoElement
|
||||
.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) {
|
||||
logger.error("Error starting camera preview:", error);
|
||||
let errorMessage =
|
||||
error instanceof Error ? error.message : "Failed to access camera";
|
||||
if (
|
||||
error.name === "NotReadableError" ||
|
||||
error.name === "TrackStartError"
|
||||
error instanceof Error &&
|
||||
(error.name === "NotReadableError" || error.name === "TrackStartError")
|
||||
) {
|
||||
errorMessage =
|
||||
"Camera is in use by another application. Please close any other apps or browser tabs using the camera and try again.";
|
||||
} else if (
|
||||
error.name === "NotAllowedError" ||
|
||||
error.name === "PermissionDeniedError"
|
||||
error instanceof Error &&
|
||||
(error.name === "NotAllowedError" ||
|
||||
error.name === "PermissionDeniedError")
|
||||
) {
|
||||
errorMessage =
|
||||
"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.cameraStateMessage = errorMessage;
|
||||
this.error = errorMessage;
|
||||
this.showCameraPreview = false;
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
@@ -526,7 +547,6 @@ export default class ImageMethodDialog extends Vue {
|
||||
},
|
||||
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 {
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
@@ -612,6 +647,7 @@ export default class ImageMethodDialog extends Vue {
|
||||
5000,
|
||||
);
|
||||
this.uploading = false;
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
formData.append("image", this.blob, this.fileName || "photo.jpg");
|
||||
@@ -666,6 +702,7 @@ export default class ImageMethodDialog extends Vue {
|
||||
);
|
||||
this.uploading = false;
|
||||
this.blob = undefined;
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -673,6 +710,14 @@ export default class ImageMethodDialog extends Vue {
|
||||
toggleDiagnostics() {
|
||||
this.showDiagnostics = !this.showDiagnostics;
|
||||
}
|
||||
|
||||
private handleQRCodeClick() {
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
this.$router.push({ name: "contact-qr-scan-full" });
|
||||
} else {
|
||||
this.$router.push({ name: "contact-qr" });
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -172,8 +172,10 @@ import {
|
||||
} from "../libs/endorserServer";
|
||||
import { decryptMessage } from "../libs/crypto";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
import * as libsUtil from "../libs/util";
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
|
||||
interface Member {
|
||||
admitted: boolean;
|
||||
@@ -209,7 +211,10 @@ export default class MembersList extends Vue {
|
||||
contacts: Array<Contact> = [];
|
||||
|
||||
async created() {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
if (USE_DEXIE_DB) {
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
}
|
||||
this.activeDid = settings.activeDid || "";
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.firstName = settings.firstName || "";
|
||||
@@ -296,7 +301,7 @@ export default class MembersList extends Vue {
|
||||
this.decryptedMembers.length === 0 ||
|
||||
this.decryptedMembers[0].member.memberId !== this.members[0].memberId
|
||||
) {
|
||||
return "Your password is not the same as the organizer. 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 {
|
||||
// the first (organizer) member was decrypted OK
|
||||
return "";
|
||||
@@ -337,7 +342,7 @@ export default class MembersList extends Vue {
|
||||
group: "alert",
|
||||
type: "info",
|
||||
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,
|
||||
);
|
||||
@@ -347,7 +352,7 @@ export default class MembersList extends Vue {
|
||||
group: "alert",
|
||||
type: "info",
|
||||
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,
|
||||
);
|
||||
@@ -355,7 +360,16 @@ export default class MembersList extends Vue {
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -439,7 +453,14 @@ export default class MembersList extends Vue {
|
||||
if (result.success) {
|
||||
decrMember.isRegistered = true;
|
||||
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;
|
||||
}
|
||||
this.$notify(
|
||||
@@ -492,7 +513,14 @@ export default class MembersList extends Vue {
|
||||
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.$notify(
|
||||
|
||||
@@ -82,12 +82,10 @@
|
||||
<script lang="ts">
|
||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import {
|
||||
createAndSubmitOffer,
|
||||
serverMessageForUser,
|
||||
} from "../libs/endorserServer";
|
||||
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||
import { createAndSubmitOffer } from "../libs/endorserServer";
|
||||
import * as libsUtil from "../libs/util";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
import { retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
@@ -116,7 +114,10 @@ export default class OfferDialog extends Vue {
|
||||
this.recipientDid = recipientDid;
|
||||
this.recipientName = recipientName;
|
||||
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
if (USE_DEXIE_DB) {
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
}
|
||||
this.apiServer = settings.apiServer || "";
|
||||
this.activeDid = settings.activeDid || "";
|
||||
|
||||
@@ -245,11 +246,8 @@ export default class OfferDialog extends Vue {
|
||||
this.projectId,
|
||||
);
|
||||
|
||||
if (
|
||||
result.type === "error" ||
|
||||
this.isOfferCreationError(result.response)
|
||||
) {
|
||||
const errorMessage = this.getOfferCreationErrorMessage(result);
|
||||
if (!result.success) {
|
||||
const errorMessage = result.error;
|
||||
logger.error("Error with offer creation result:", result);
|
||||
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>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<h1 class="text-xl font-bold text-center mb-4 relative">
|
||||
Welcome to Time Safari
|
||||
<br />
|
||||
- Showcasing Gratitude & Magnifying Time
|
||||
- Showcase Impact & Magnify Time
|
||||
<div
|
||||
class="text-lg text-center leading-none absolute right-0 -top-1"
|
||||
@click="onClickClose(true)"
|
||||
@@ -14,6 +14,9 @@
|
||||
</div>
|
||||
</h1>
|
||||
|
||||
The feed underneath this pop-up shows the latest contributions, some from
|
||||
people and some from projects.
|
||||
|
||||
<p v-if="isRegistered" class="mt-4">
|
||||
You can now log things that you've seen:
|
||||
<span v-if="numContacts > 0">
|
||||
@@ -23,14 +26,10 @@
|
||||
<span class="bg-green-600 text-white rounded-full">
|
||||
<font-awesome icon="plus" class="fa-fw" />
|
||||
</span>
|
||||
button to express your appreciation for... whatever -- maybe thanks for
|
||||
showing you all these fascinating stories of
|
||||
<em>gratitude</em>.
|
||||
button to express your appreciation for... whatever.
|
||||
</p>
|
||||
<p v-else class="mt-4">
|
||||
The feed underneath this pop-up shows the latest gifts that others have
|
||||
recognized. Once someone registers you, you can log your appreciation,
|
||||
too.
|
||||
<p class="mt-4">
|
||||
Once someone registers you, you can log your appreciation, too.
|
||||
</p>
|
||||
|
||||
<p class="mt-4">
|
||||
@@ -201,13 +200,16 @@
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
import { Router } from "vue-router";
|
||||
|
||||
import { NotificationIface } from "../constants/app";
|
||||
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||
import {
|
||||
db,
|
||||
retrieveSettingsForActiveAccount,
|
||||
updateAccountSettings,
|
||||
} from "../db/index";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
import { OnboardPage } from "../libs/util";
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
import { Contact } from "@/db/tables/contacts";
|
||||
|
||||
@Component({
|
||||
computed: {
|
||||
@@ -222,7 +224,7 @@ export default class OnboardingDialog extends Vue {
|
||||
$router!: Router;
|
||||
|
||||
activeDid = "";
|
||||
firstContactName = null;
|
||||
firstContactName = "";
|
||||
givenName = "";
|
||||
isRegistered = false;
|
||||
numContacts = 0;
|
||||
@@ -231,29 +233,54 @@ export default class OnboardingDialog extends Vue {
|
||||
|
||||
async open(page: OnboardPage) {
|
||||
this.page = page;
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
if (USE_DEXIE_DB) {
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
}
|
||||
this.activeDid = settings.activeDid || "";
|
||||
this.isRegistered = !!settings.isRegistered;
|
||||
const contacts = await db.contacts.toArray();
|
||||
this.numContacts = contacts.length;
|
||||
if (this.numContacts > 0) {
|
||||
this.firstContactName = contacts[0].name;
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const dbContacts = await platformService.dbQuery("SELECT * FROM contacts");
|
||||
if (dbContacts) {
|
||||
this.numContacts = dbContacts.values.length;
|
||||
const firstContact = dbContacts.values[0];
|
||||
const fullContact = databaseUtil.mapColumnsToValues(dbContacts.columns, [
|
||||
firstContact,
|
||||
]) as unknown as Contact;
|
||||
this.firstContactName = fullContact.name || "";
|
||||
}
|
||||
if (USE_DEXIE_DB) {
|
||||
const contacts = await db.contacts.toArray();
|
||||
this.numContacts = contacts.length;
|
||||
if (this.numContacts > 0) {
|
||||
this.firstContactName = contacts[0].name || "";
|
||||
}
|
||||
}
|
||||
this.visible = true;
|
||||
if (this.page === OnboardPage.Create) {
|
||||
// we'll assume that they've been through all the other pages
|
||||
await updateAccountSettings(this.activeDid, {
|
||||
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
|
||||
finishedOnboarding: true,
|
||||
});
|
||||
if (USE_DEXIE_DB) {
|
||||
await updateAccountSettings(this.activeDid, {
|
||||
finishedOnboarding: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async onClickClose(done?: boolean, goHome?: boolean) {
|
||||
this.visible = false;
|
||||
if (done) {
|
||||
await updateAccountSettings(this.activeDid, {
|
||||
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
|
||||
finishedOnboarding: true,
|
||||
});
|
||||
if (USE_DEXIE_DB) {
|
||||
await updateAccountSettings(this.activeDid, {
|
||||
finishedOnboarding: true,
|
||||
});
|
||||
}
|
||||
if (goHome) {
|
||||
this.$router.push({ name: "home" });
|
||||
}
|
||||
|
||||
@@ -119,7 +119,12 @@ PhotoDialog.vue */
|
||||
import axios from "axios";
|
||||
import { Component, Vue } from "vue-facing-decorator";
|
||||
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 { accessToken } from "../libs/crypto";
|
||||
import { logger } from "../utils/logger";
|
||||
@@ -173,9 +178,12 @@ export default class PhotoDialog extends Vue {
|
||||
* @throws {Error} When settings retrieval fails
|
||||
*/
|
||||
async mounted() {
|
||||
logger.log("PhotoDialog mounted");
|
||||
// logger.log("PhotoDialog mounted");
|
||||
try {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
if (USE_DEXIE_DB) {
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
}
|
||||
this.activeDid = settings.activeDid || "";
|
||||
this.isRegistered = !!settings.isRegistered;
|
||||
logger.log("isRegistered:", this.isRegistered);
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<template>
|
||||
<a
|
||||
v-if="linkToFull && imageUrl"
|
||||
v-if="linkToFullImage && imageUrl"
|
||||
:href="imageUrl"
|
||||
target="_blank"
|
||||
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>
|
||||
<div
|
||||
v-else
|
||||
class="h-full w-full object-contain"
|
||||
v-html="generateIdenticon()"
|
||||
/>
|
||||
<div v-else class="h-full w-full object-contain" v-html="generateIcon()" />
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { toSvg } from "jdenticon";
|
||||
@@ -35,9 +31,9 @@ export default class ProjectIcon extends Vue {
|
||||
@Prop entityId = "";
|
||||
@Prop iconSize = 0;
|
||||
@Prop imageUrl = "";
|
||||
@Prop linkToFull = false;
|
||||
@Prop linkToFullImage = false;
|
||||
|
||||
generateIdenticon() {
|
||||
generateIcon() {
|
||||
if (this.imageUrl) {
|
||||
return `<img src="${this.imageUrl}" class="w-full h-full object-contain" />`;
|
||||
} else {
|
||||
|
||||
@@ -102,7 +102,12 @@
|
||||
<script lang="ts">
|
||||
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 {
|
||||
logConsoleAndDb,
|
||||
retrieveSettingsForActiveAccount,
|
||||
@@ -169,7 +174,10 @@ export default class PushNotificationPermission extends Vue {
|
||||
this.isVisible = true;
|
||||
this.pushType = pushType;
|
||||
try {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
if (USE_DEXIE_DB) {
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
}
|
||||
let pushUrl = DEFAULT_PUSH_SERVER;
|
||||
if (settings?.webPushServer) {
|
||||
pushUrl = settings.webPushServer;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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="ml-2">
|
||||
<router-link
|
||||
@@ -15,7 +15,8 @@
|
||||
<script lang="ts">
|
||||
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";
|
||||
|
||||
@Component
|
||||
@@ -28,20 +29,22 @@ export default class TopMessage extends Vue {
|
||||
|
||||
async mounted() {
|
||||
try {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
if (USE_DEXIE_DB) {
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
}
|
||||
if (
|
||||
settings.warnIfTestServer &&
|
||||
settings.apiServer !== AppString.PROD_ENDORSER_API_SERVER
|
||||
) {
|
||||
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 (
|
||||
settings.warnIfProdServer &&
|
||||
settings.apiServer === AppString.PROD_ENDORSER_API_SERVER
|
||||
) {
|
||||
const didPrefix = settings.activeDid?.slice(11, 15);
|
||||
this.message =
|
||||
"You're linked to the production server, user " + didPrefix;
|
||||
this.message = "You are using prod, user " + didPrefix;
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
this.$notify(
|
||||
|
||||
@@ -37,9 +37,11 @@
|
||||
<script lang="ts">
|
||||
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 * as databaseUtil from "../db/databaseUtil";
|
||||
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
|
||||
@Component
|
||||
export default class UserNameDialog extends Vue {
|
||||
@@ -61,15 +63,25 @@ export default class UserNameDialog extends Vue {
|
||||
*/
|
||||
async open(aCallback?: (name?: string) => void) {
|
||||
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.visible = true;
|
||||
}
|
||||
|
||||
async onClickSaveChanges() {
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
firstName: this.givenName,
|
||||
});
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
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.callback(this.givenName);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ import * as THREE from "three";
|
||||
import { GLTFLoader } from "three/addons/loaders/GLTFLoader";
|
||||
import * as SkeletonUtils from "three/addons/utils/SkeletonUtils";
|
||||
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 { getHeaders } from "../../../../libs/endorserServer";
|
||||
import { logger } from "../../../../utils/logger";
|
||||
@@ -14,7 +16,10 @@ export async function loadLandmarks(vue, world, scene, loop) {
|
||||
vue.setWorldProperty("animationDurationSeconds", ANIMATION_DURATION_SECS);
|
||||
|
||||
try {
|
||||
const settings = await retrieveSettingsForActiveAccount();
|
||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
if (USE_DEXIE_DB) {
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
}
|
||||
const activeDid = settings.activeDid || "";
|
||||
const apiServer = settings.apiServer;
|
||||
const headers = await getHeaders(activeDid);
|
||||
|
||||
@@ -7,6 +7,7 @@ export enum AppString {
|
||||
// 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.
|
||||
APP_NAME = "Time Safari",
|
||||
APP_NAME_NO_SPACES = "TimeSafari",
|
||||
|
||||
PROD_ENDORSER_API_SERVER = "https://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 =
|
||||
import.meta.env.VITE_DEFAULT_ENDORSER_API_SERVER ||
|
||||
AppString.TEST_ENDORSER_API_SERVER;
|
||||
AppString.PROD_ENDORSER_API_SERVER;
|
||||
|
||||
export const 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 =
|
||||
import.meta.env.VITE_DEFAULT_PARTNER_API_SERVER ||
|
||||
AppString.TEST_PARTNER_API_SERVER;
|
||||
AppString.PROD_PARTNER_API_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 PASSKEYS_ENABLED =
|
||||
!!import.meta.env.VITE_PASSKEYS_ENABLED || false;
|
||||
|
||||
export const USE_DEXIE_DB = false;
|
||||
|
||||
/**
|
||||
* The possible values for "group" and "type" are in App.vue.
|
||||
* Some of this comes from the notiwind package, some is custom.
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
import migrationService from "../services/migrationService";
|
||||
import type { QueryExecResult, SqlValue } from "../interfaces/database";
|
||||
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
|
||||
import { arrayBufferToBase64 } from "@/libs/crypto";
|
||||
|
||||
// Generate a random secret for the secret table
|
||||
|
||||
// It's not really secure to maintain the secret next to the user's data.
|
||||
// However, until we have better hooks into a real wallet or reliable secure
|
||||
// storage, we'll do this for user convenience. As they sign more records
|
||||
// and integrate with more people, they'll value it more and want to be more
|
||||
// secure, so we'll prompt them to take steps to back it up, properly encrypt,
|
||||
// etc. At the beginning, we'll prompt for a password, then we'll prompt for a
|
||||
// PWA so it's not in a browser... and then we hope to be integrated with a
|
||||
// real wallet or something else more secure.
|
||||
|
||||
// One might ask: why encrypt at all? We figure a basic encryption is better
|
||||
// than none. Plus, we expect to support their own password or keystore or
|
||||
// external wallet as better signing options in the future, so it's gonna be
|
||||
// important to have the structure where each account access might require
|
||||
// user action.
|
||||
|
||||
// (Once upon a time we stored the secret in localStorage, but it frequently
|
||||
// got erased, even though the IndexedDB still had the identity data. This
|
||||
// ended up throwing lots of errors to the user... and they'd end up in a state
|
||||
// where they couldn't take action because they couldn't unlock that identity.)
|
||||
|
||||
const randomBytes = crypto.getRandomValues(new Uint8Array(32));
|
||||
const secretBase64 = arrayBufferToBase64(randomBytes);
|
||||
|
||||
// Each migration can include multiple SQL statements (with semicolons)
|
||||
const MIGRATIONS = [
|
||||
@@ -12,8 +38,8 @@ const MIGRATIONS = [
|
||||
dateCreated TEXT NOT NULL,
|
||||
derivationPath TEXT,
|
||||
did TEXT NOT NULL,
|
||||
identity TEXT,
|
||||
mnemonic TEXT,
|
||||
identityEncrBase64 TEXT, -- encrypted & base64-encoded
|
||||
mnemonicEncrBase64 TEXT, -- encrypted & base64-encoded
|
||||
passkeyCredIdHex TEXT,
|
||||
publicKeyHex TEXT NOT NULL
|
||||
);
|
||||
@@ -22,9 +48,11 @@ const MIGRATIONS = [
|
||||
|
||||
CREATE TABLE IF NOT EXISTS secret (
|
||||
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 (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
accountDid TEXT,
|
||||
@@ -59,6 +87,8 @@ const MIGRATIONS = [
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_settings_accountDid ON settings(accountDid);
|
||||
|
||||
INSERT INTO settings (id, apiServer) VALUES (1, '${DEFAULT_ENDORSER_API_SERVER}');
|
||||
|
||||
CREATE TABLE IF NOT EXISTS contacts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
did TEXT NOT NULL,
|
||||
@@ -76,7 +106,7 @@ const MIGRATIONS = [
|
||||
CREATE INDEX IF NOT EXISTS idx_contacts_name ON contacts(name);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS logs (
|
||||
date TEXT PRIMARY KEY,
|
||||
date TEXT NOT NULL,
|
||||
message TEXT NOT NULL
|
||||
);
|
||||
|
||||
@@ -88,19 +118,21 @@ const MIGRATIONS = [
|
||||
},
|
||||
];
|
||||
|
||||
export async function registerMigrations(): Promise<void> {
|
||||
// Register all migrations
|
||||
for (const migration of MIGRATIONS) {
|
||||
await migrationService.registerMigration(migration);
|
||||
}
|
||||
}
|
||||
|
||||
export async function runMigrations(
|
||||
sqlExec: (
|
||||
sql: string,
|
||||
params?: SqlValue[],
|
||||
) => Promise<Array<QueryExecResult>>,
|
||||
/**
|
||||
* @param sqlExec - A function that executes a SQL statement and returns the result
|
||||
* @param extractMigrationNames - A function that extracts the names (string array) from "select name from migrations"
|
||||
*/
|
||||
export async function runMigrations<T>(
|
||||
sqlExec: (sql: string) => Promise<unknown>,
|
||||
sqlQuery: (sql: string) => Promise<T>,
|
||||
extractMigrationNames: (result: T) => Set<string>,
|
||||
): Promise<void> {
|
||||
await registerMigrations();
|
||||
await migrationService.runMigrations(sqlExec);
|
||||
for (const migration of MIGRATIONS) {
|
||||
migrationService.registerMigration(migration);
|
||||
}
|
||||
await migrationService.runMigrations(
|
||||
sqlExec,
|
||||
sqlQuery,
|
||||
extractMigrationNames,
|
||||
);
|
||||
}
|
||||
|
||||
426
src/db/databaseUtil.ts
Normal file
@@ -0,0 +1,426 @@
|
||||
/**
|
||||
* 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 an SQL INSERT statement and parameters from a model object.
|
||||
* @param model The model object containing fields to update
|
||||
* @param tableName The name of the table to update
|
||||
* @returns Object containing the SQL statement and parameters array
|
||||
*/
|
||||
export function generateInsertStatement(
|
||||
model: Record<string, unknown>,
|
||||
tableName: string,
|
||||
): { sql: string; params: unknown[] } {
|
||||
const columns = Object.keys(model).filter((key) => model[key] !== undefined);
|
||||
const values = Object.values(model).filter((value) => value !== undefined);
|
||||
const placeholders = values.map(() => "?").join(", ");
|
||||
const insertSql = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${placeholders})`;
|
||||
|
||||
return {
|
||||
sql: insertSql,
|
||||
params: values,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an SQL UPDATE statement and parameters from a model object.
|
||||
* @param model The model object containing fields to update
|
||||
* @param tableName The name of the table to update
|
||||
* @param whereClause The WHERE clause for the update (e.g. "id = ?")
|
||||
* @param whereParams Parameters for the WHERE clause
|
||||
* @returns Object containing the SQL statement and parameters array
|
||||
*/
|
||||
export function generateUpdateStatement(
|
||||
model: Record<string, unknown>,
|
||||
tableName: string,
|
||||
whereClause: string,
|
||||
whereParams: unknown[] = [],
|
||||
): { sql: string; params: unknown[] } {
|
||||
// Filter out undefined/null values and create SET clause
|
||||
const setClauses: string[] = [];
|
||||
const params: unknown[] = [];
|
||||
|
||||
Object.entries(model).forEach(([key, value]) => {
|
||||
setClauses.push(`${key} = ?`);
|
||||
params.push(value ?? null);
|
||||
});
|
||||
|
||||
if (setClauses.length === 0) {
|
||||
throw new Error("No valid fields to update");
|
||||
}
|
||||
|
||||
const sql = `UPDATE ${tableName} SET ${setClauses.join(", ")} WHERE ${whereClause}`;
|
||||
|
||||
return {
|
||||
sql,
|
||||
params: [...params, ...whereParams],
|
||||
};
|
||||
}
|
||||
|
||||
export function mapQueryResultToValues(
|
||||
record: QueryExecResult | undefined,
|
||||
): Array<Record<string, unknown>> {
|
||||
if (!record) {
|
||||
return [];
|
||||
}
|
||||
return mapColumnsToValues(record.columns, record.values) as Array<
|
||||
Record<string, unknown>
|
||||
>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps an array of column names to an array of value arrays, creating objects where each column name
|
||||
* is mapped to its corresponding value.
|
||||
* @param columns Array of column names to use as object keys
|
||||
* @param values Array of value arrays, where each inner array corresponds to one row of data
|
||||
* @returns Array of objects where each object maps column names to their corresponding values
|
||||
*/
|
||||
export function mapColumnsToValues(
|
||||
columns: string[],
|
||||
values: unknown[][],
|
||||
): Array<Record<string, unknown>> {
|
||||
return values.map((row) => {
|
||||
const obj: Record<string, unknown> = {};
|
||||
columns.forEach((column, index) => {
|
||||
obj[column] = row[index];
|
||||
});
|
||||
return obj;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 { encrypted, Encryption } from "@pvermeer/dexie-encrypted-addon";
|
||||
import * as R from "ramda";
|
||||
@@ -26,8 +32,8 @@ type NonsensitiveTables = {
|
||||
};
|
||||
|
||||
// Using 'unknown' instead of 'any' for stricter typing and to avoid TypeScript warnings
|
||||
export type SecretDexie<T extends unknown = SecretTable> = BaseDexie & T;
|
||||
export type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T;
|
||||
type SecretDexie<T extends unknown = SecretTable> = BaseDexie & T;
|
||||
type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T;
|
||||
export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> =
|
||||
BaseDexie & T;
|
||||
|
||||
|
||||
@@ -45,6 +45,12 @@ export type Account = {
|
||||
publicKeyHex: string;
|
||||
};
|
||||
|
||||
// When finished with USE_DEXIE_DB, move these fields to Account and move identity and mnemonic here.
|
||||
export type AccountEncrypted = Account & {
|
||||
identityEncrBase64: string;
|
||||
mnemonicEncrBase64: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Schema for the accounts table in the database.
|
||||
* Fields starting with a $ character are encrypted.
|
||||
|
||||
@@ -25,6 +25,25 @@ function createWindow(): void {
|
||||
logger.log("Checking preload path:", preloadPath);
|
||||
logger.log("Preload exists:", fs.existsSync(preloadPath));
|
||||
|
||||
// Log environment and paths
|
||||
logger.log("process.cwd():", process.cwd());
|
||||
logger.log("__dirname:", __dirname);
|
||||
logger.log("app.getAppPath():", app.getAppPath());
|
||||
logger.log("app.isPackaged:", app.isPackaged);
|
||||
|
||||
// List files in __dirname and __dirname/www
|
||||
try {
|
||||
logger.log("Files in __dirname:", fs.readdirSync(__dirname));
|
||||
const wwwDir = path.join(__dirname, "www");
|
||||
if (fs.existsSync(wwwDir)) {
|
||||
logger.log("Files in www:", fs.readdirSync(wwwDir));
|
||||
} else {
|
||||
logger.log("www directory does not exist in __dirname");
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error("Error reading directories:", e);
|
||||
}
|
||||
|
||||
// Create the browser window.
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
@@ -88,7 +107,16 @@ function createWindow(): void {
|
||||
logger.log("process.cwd():", process.cwd());
|
||||
}
|
||||
|
||||
const indexPath = path.join(__dirname, "www", "index.html");
|
||||
let indexPath = path.resolve(__dirname, "dist-electron", "www", "index.html");
|
||||
if (!fs.existsSync(indexPath)) {
|
||||
// Fallback for dev mode
|
||||
indexPath = path.resolve(
|
||||
process.cwd(),
|
||||
"dist-electron",
|
||||
"www",
|
||||
"index.html",
|
||||
);
|
||||
}
|
||||
|
||||
if (isDev) {
|
||||
logger.log("Loading index from:", indexPath);
|
||||
|
||||
@@ -2,24 +2,33 @@ const { contextBridge, ipcRenderer } = require("electron");
|
||||
|
||||
const logger = {
|
||||
log: (message, ...args) => {
|
||||
// Always log in development, log with context in production
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
/* eslint-disable no-console */
|
||||
console.log(message, ...args);
|
||||
console.log(`[Preload] ${message}`, ...args);
|
||||
/* eslint-enable no-console */
|
||||
}
|
||||
},
|
||||
warn: (message, ...args) => {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
/* eslint-disable no-console */
|
||||
console.warn(message, ...args);
|
||||
/* eslint-enable no-console */
|
||||
}
|
||||
// Always log warnings
|
||||
/* eslint-disable no-console */
|
||||
console.warn(`[Preload] ${message}`, ...args);
|
||||
/* eslint-enable no-console */
|
||||
},
|
||||
error: (message, ...args) => {
|
||||
// Always log errors
|
||||
/* eslint-disable no-console */
|
||||
console.error(message, ...args); // Errors should always be logged
|
||||
console.error(`[Preload] ${message}`, ...args);
|
||||
/* eslint-enable no-console */
|
||||
},
|
||||
info: (message, ...args) => {
|
||||
// Always log info in development, log with context in production
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
/* eslint-disable no-console */
|
||||
console.info(`[Preload] ${message}`, ...args);
|
||||
/* eslint-enable no-console */
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Use a more direct path resolution approach
|
||||
@@ -41,7 +50,10 @@ const getPath = (pathType) => {
|
||||
}
|
||||
};
|
||||
|
||||
logger.log("Preload script starting...");
|
||||
logger.info("Preload script starting...");
|
||||
|
||||
// Force electron platform in the renderer process
|
||||
window.process = { env: { VITE_PLATFORM: "electron" } };
|
||||
|
||||
try {
|
||||
contextBridge.exposeInMainWorld("electronAPI", {
|
||||
@@ -65,6 +77,7 @@ try {
|
||||
env: {
|
||||
isElectron: true,
|
||||
isDev: process.env.NODE_ENV === "development",
|
||||
platform: "electron", // Explicitly set platform
|
||||
},
|
||||
// Path utilities
|
||||
getBasePath: () => {
|
||||
@@ -72,7 +85,7 @@ try {
|
||||
},
|
||||
});
|
||||
|
||||
logger.log("Preload script completed successfully");
|
||||
logger.info("Preload script completed successfully");
|
||||
} catch (error) {
|
||||
logger.error("Error in preload script:", error);
|
||||
}
|
||||
|
||||
59
src/interfaces/absurd-sql.d.ts
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { QueryExecResult, SqlValue } from "./database";
|
||||
|
||||
declare module "@jlongster/sql.js" {
|
||||
interface SQL {
|
||||
Database: new (path: string, options?: { filename: boolean }) => AbsurdSqlDatabase;
|
||||
FS: {
|
||||
mkdir: (path: string) => void;
|
||||
mount: (fs: any, options: any, path: string) => void;
|
||||
open: (path: string, flags: string) => any;
|
||||
close: (stream: any) => void;
|
||||
};
|
||||
register_for_idb: (fs: any) => void;
|
||||
}
|
||||
|
||||
interface AbsurdSqlDatabase {
|
||||
exec: (sql: string, params?: unknown[]) => Promise<QueryExecResult[]>;
|
||||
run: (
|
||||
sql: string,
|
||||
params?: unknown[],
|
||||
) => Promise<{ changes: number; lastId?: number }>;
|
||||
}
|
||||
|
||||
const initSqlJs: (options?: {
|
||||
locateFile?: (file: string) => string;
|
||||
}) => Promise<SQL>;
|
||||
|
||||
export default initSqlJs;
|
||||
}
|
||||
|
||||
declare module "absurd-sql" {
|
||||
import type { SQL } from "@jlongster/sql.js";
|
||||
|
||||
export class SQLiteFS {
|
||||
constructor(fs: any, backend: any);
|
||||
}
|
||||
}
|
||||
|
||||
declare module "absurd-sql/dist/indexeddb-backend" {
|
||||
export default class IndexedDBBackend {
|
||||
constructor();
|
||||
}
|
||||
}
|
||||
|
||||
declare module "absurd-sql/dist/indexeddb-main-thread" {
|
||||
export interface SQLiteOptions {
|
||||
filename?: string;
|
||||
autoLoad?: boolean;
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
export interface SQLiteDatabase {
|
||||
exec: (sql: string, params?: unknown[]) => Promise<QueryExecResult[]>;
|
||||
close: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function initSqlJs(options?: any): Promise<any>;
|
||||
export function createDatabase(options?: SQLiteOptions): Promise<SQLiteDatabase>;
|
||||
export function openDatabase(options?: SQLiteOptions): Promise<SQLiteDatabase>;
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
import { AxiosResponse } from "axios";
|
||||
import { GiverReceiverInputInfo } from "../libs/util";
|
||||
import { ErrorResult, ResultWithType } from "./common";
|
||||
|
||||
export interface GiverOutputInfo {
|
||||
action: string;
|
||||
@@ -47,12 +45,3 @@ export interface ProviderInfo {
|
||||
*/
|
||||
linkConfirmed: boolean;
|
||||
}
|
||||
|
||||
// Type for createAndSubmitClaim result
|
||||
export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult;
|
||||
|
||||
// Update SuccessResult to use ClaimResult
|
||||
export interface SuccessResult extends ResultWithType {
|
||||
type: "success";
|
||||
response: AxiosResponse<ClaimResult>;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
import { GenericVerifiableCredential } from "./common";
|
||||
/**
|
||||
* Types of Claims
|
||||
*
|
||||
* Note that these are for the claims that get signed.
|
||||
* Records that are the latest edited entities are in the records.ts file.
|
||||
*
|
||||
*/
|
||||
|
||||
export interface AgreeVerifiableCredential {
|
||||
"@context": string;
|
||||
import { ClaimObject } from "./common";
|
||||
|
||||
export interface AgreeActionClaim extends ClaimObject {
|
||||
"@context": "https://schema.org";
|
||||
"@type": string;
|
||||
object: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// Note that previous VCs may have additional fields.
|
||||
// https://endorser.ch/doc/html/transactions.html#id4
|
||||
export interface GiveVerifiableCredential extends GenericVerifiableCredential {
|
||||
"@context"?: string;
|
||||
export interface GiveActionClaim extends ClaimObject {
|
||||
// context is optional because it might be embedded in another claim, eg. an AgreeAction
|
||||
"@context"?: "https://schema.org";
|
||||
"@type": "GiveAction";
|
||||
agent?: { identifier: string };
|
||||
description?: string;
|
||||
@@ -17,16 +26,25 @@ export interface GiveVerifiableCredential extends GenericVerifiableCredential {
|
||||
identifier?: string;
|
||||
image?: string;
|
||||
object?: { amountOfThisGood: number; unitCode: string };
|
||||
provider?: GenericVerifiableCredential;
|
||||
provider?: ClaimObject;
|
||||
recipient?: { identifier: string };
|
||||
}
|
||||
|
||||
export interface JoinActionClaim extends ClaimObject {
|
||||
agent?: { identifier: string };
|
||||
event?: { organizer?: { name: string }; name?: string; startTime?: string };
|
||||
}
|
||||
|
||||
// Note that previous VCs may have additional fields.
|
||||
// https://endorser.ch/doc/html/transactions.html#id8
|
||||
export interface OfferVerifiableCredential extends GenericVerifiableCredential {
|
||||
"@context"?: string;
|
||||
export interface OfferClaim extends ClaimObject {
|
||||
"@context": "https://schema.org";
|
||||
"@type": "Offer";
|
||||
agent?: { identifier: string };
|
||||
description?: string;
|
||||
fulfills?: { "@type": string; identifier?: string; lastClaimId?: string }[];
|
||||
identifier?: string;
|
||||
image?: string;
|
||||
includesObject?: { amountOfThisGood: number; unitCode: string };
|
||||
itemOffered?: {
|
||||
description?: string;
|
||||
@@ -37,14 +55,18 @@ export interface OfferVerifiableCredential extends GenericVerifiableCredential {
|
||||
name?: string;
|
||||
};
|
||||
};
|
||||
offeredBy?: { identifier: string };
|
||||
offeredBy?: {
|
||||
type?: "Person";
|
||||
identifier: string;
|
||||
};
|
||||
provider?: ClaimObject;
|
||||
recipient?: { identifier: string };
|
||||
validThrough?: string;
|
||||
}
|
||||
|
||||
// Note that previous VCs may have additional fields.
|
||||
// https://endorser.ch/doc/html/transactions.html#id7
|
||||
export interface PlanVerifiableCredential extends GenericVerifiableCredential {
|
||||
export interface PlanActionClaim extends ClaimObject {
|
||||
"@context": "https://schema.org";
|
||||
"@type": "PlanAction";
|
||||
name: string;
|
||||
@@ -58,11 +80,18 @@ export interface PlanVerifiableCredential extends GenericVerifiableCredential {
|
||||
}
|
||||
|
||||
// AKA Registration & RegisterAction
|
||||
export interface RegisterVerifiableCredential {
|
||||
"@context": string;
|
||||
export interface RegisterActionClaim extends ClaimObject {
|
||||
"@context": "https://schema.org";
|
||||
"@type": "RegisterAction";
|
||||
agent: { identifier: string };
|
||||
identifier?: string;
|
||||
object: string;
|
||||
object?: string;
|
||||
participant?: { identifier: string };
|
||||
}
|
||||
|
||||
export interface TenureClaim extends ClaimObject {
|
||||
"@context": "https://endorser.ch";
|
||||
"@type": "Tenure";
|
||||
party?: { identifier: string };
|
||||
spatialUnit?: { geo?: { polygon?: string } };
|
||||
}
|
||||
|
||||
@@ -15,10 +15,6 @@ export interface GenericCredWrapper<T extends GenericVerifiableCredential> {
|
||||
publicUrls?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ResultWithType {
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface ErrorResponse {
|
||||
error?: {
|
||||
message?: string;
|
||||
@@ -30,7 +26,76 @@ export interface InternalError {
|
||||
userMessage?: string;
|
||||
}
|
||||
|
||||
export interface ErrorResult extends ResultWithType {
|
||||
type: "error";
|
||||
error: InternalError;
|
||||
export interface KeyMeta {
|
||||
did: string;
|
||||
publicKeyHex: string;
|
||||
derivationPath?: string;
|
||||
passkeyCredIdHex?: string; // The Webauthn credential ID in hex, if this is from a passkey
|
||||
}
|
||||
|
||||
export interface KeyMetaMaybeWithPrivate extends KeyMeta {
|
||||
mnemonic?: string; // 12 or 24 words encoding the seed
|
||||
identity?: string; // Stringified IIdentifier object from Veramo
|
||||
}
|
||||
|
||||
export interface KeyMetaWithPrivate extends KeyMeta {
|
||||
mnemonic: string; // 12 or 24 words encoding the seed
|
||||
identity: string; // Stringified IIdentifier object from Veramo
|
||||
}
|
||||
|
||||
export interface QuantitativeValue extends GenericVerifiableCredential {
|
||||
"@type": "QuantitativeValue";
|
||||
"@context"?: string;
|
||||
amountOfThisGood: number;
|
||||
unitCode: string;
|
||||
}
|
||||
|
||||
export interface AxiosErrorResponse {
|
||||
message?: string;
|
||||
response?: {
|
||||
data?: {
|
||||
error?: {
|
||||
message?: string;
|
||||
};
|
||||
[key: string]: unknown;
|
||||
};
|
||||
status?: number;
|
||||
config?: unknown;
|
||||
};
|
||||
config?: unknown;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface UserInfo {
|
||||
did: string;
|
||||
name: string;
|
||||
publicEncKey: string;
|
||||
registered: boolean;
|
||||
profileImageUrl?: string;
|
||||
nextPublicEncKeyHash?: string;
|
||||
}
|
||||
|
||||
export interface CreateAndSubmitClaimResult {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
handleId?: string;
|
||||
}
|
||||
|
||||
export interface Agent {
|
||||
identifier?: string;
|
||||
did?: string;
|
||||
}
|
||||
|
||||
export interface ClaimObject {
|
||||
"@type": string;
|
||||
"@context"?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface VerifiableCredentialClaim {
|
||||
"@context"?: string;
|
||||
"@type": string;
|
||||
type: string[];
|
||||
credentialSubject: ClaimObject;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,4 @@ export interface DatabaseService {
|
||||
sql: string,
|
||||
params?: unknown[],
|
||||
): Promise<{ changes: number; lastId?: number }>;
|
||||
getOneRow(sql: string, params?: unknown[]): Promise<unknown[] | undefined>;
|
||||
getAll(sql: string, params?: unknown[]): Promise<unknown[][]>;
|
||||
}
|
||||
|
||||
@@ -29,18 +29,17 @@ import { z } from "zod";
|
||||
|
||||
// Add a union type of all valid route paths
|
||||
export const VALID_DEEP_LINK_ROUTES = [
|
||||
"user-profile",
|
||||
"project-details",
|
||||
"onboard-meeting-setup",
|
||||
"invite-one-accept",
|
||||
"contact-import",
|
||||
"confirm-gift",
|
||||
// note that similar lists are below in deepLinkSchemas and in src/services/deepLinks.ts
|
||||
"claim",
|
||||
"claim-cert",
|
||||
"claim-add-raw",
|
||||
"contact-edit",
|
||||
"contacts",
|
||||
"claim-cert",
|
||||
"confirm-gift",
|
||||
"contact-import",
|
||||
"did",
|
||||
"invite-one-accept",
|
||||
"onboard-meeting-setup",
|
||||
"project",
|
||||
"user-profile",
|
||||
] as const;
|
||||
|
||||
// Create a type from the array
|
||||
@@ -58,44 +57,39 @@ export const routeSchema = z.enum(VALID_DEEP_LINK_ROUTES);
|
||||
|
||||
// Parameter validation schemas for each route type
|
||||
export const deepLinkSchemas = {
|
||||
"user-profile": z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
"project-details": z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
"onboard-meeting-setup": z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
"invite-one-accept": z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
"contact-import": z.object({
|
||||
jwt: z.string(),
|
||||
}),
|
||||
"confirm-gift": z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
// note that similar lists are above in VALID_DEEP_LINK_ROUTES and in src/services/deepLinks.ts
|
||||
claim: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
"claim-cert": z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
"claim-add-raw": z.object({
|
||||
id: z.string(),
|
||||
claim: z.string().optional(),
|
||||
claimJwtId: z.string().optional(),
|
||||
}),
|
||||
"contact-edit": z.object({
|
||||
did: z.string(),
|
||||
"claim-cert": z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
contacts: z.object({
|
||||
contacts: z.string(), // JSON string of contacts array
|
||||
"confirm-gift": z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
"contact-import": z.object({
|
||||
jwt: z.string(),
|
||||
}),
|
||||
did: z.object({
|
||||
did: z.string(),
|
||||
}),
|
||||
"invite-one-accept": z.object({
|
||||
jwt: z.string(),
|
||||
}),
|
||||
"onboard-meeting-setup": z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
project: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
"user-profile": z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
};
|
||||
|
||||
export type DeepLinkParams = {
|
||||
|
||||