Compare commits
45 Commits
ui-fixes-2
...
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 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -55,3 +55,4 @@ build_logs/
|
|||||||
icons
|
icons
|
||||||
|
|
||||||
|
|
||||||
|
android/app/src/main/res/
|
||||||
74
BUILDING.md
74
BUILDING.md
@@ -9,19 +9,6 @@ For a quick dev environment setup, use [pkgx](https://pkgx.dev).
|
|||||||
- Node.js (LTS version recommended)
|
- Node.js (LTS version recommended)
|
||||||
- npm (comes with Node.js)
|
- npm (comes with Node.js)
|
||||||
- Git
|
- Git
|
||||||
- For Android builds: Android Studio with SDK installed
|
|
||||||
- For iOS builds: macOS with Xcode and ruby gems & bundle
|
|
||||||
- `pkgx +rubygems.org sh`
|
|
||||||
|
|
||||||
- ... and you may have to fix these, especially with pkgx
|
|
||||||
|
|
||||||
```bash
|
|
||||||
gem_path=$(which gem)
|
|
||||||
shortened_path="${gem_path:h:h}"
|
|
||||||
export GEM_HOME=$shortened_path
|
|
||||||
export GEM_PATH=$shortened_path
|
|
||||||
```
|
|
||||||
|
|
||||||
- For desktop builds: Additional build tools based on your OS
|
- For desktop builds: Additional build tools based on your OS
|
||||||
|
|
||||||
## Forks
|
## Forks
|
||||||
@@ -54,6 +41,7 @@ Install dependencies:
|
|||||||
1. Run the production build:
|
1. Run the production build:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
rm -rf dist
|
||||||
npm run build:web
|
npm run build:web
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -77,6 +65,8 @@ Install dependencies:
|
|||||||
|
|
||||||
* Commit everything (since the commit hash is used the app).
|
* 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).
|
* 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`.
|
* 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):
|
* For test, build the app (because test server is not yet set up to build):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_DEFAULT_PUSH_SERVER=https://test.timesafari.app VITE_PASSKEYS_ENABLED=true npm run build
|
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_DEFAULT_PUSH_SERVER=https://test.timesafari.app VITE_PASSKEYS_ENABLED=true npm run build:web
|
||||||
```
|
```
|
||||||
|
|
||||||
... and transfer to the test server:
|
... and transfer to the test server:
|
||||||
@@ -326,17 +316,33 @@ npm run build:electron-prod && npm run electron:start
|
|||||||
|
|
||||||
Prerequisites: macOS with Xcode installed
|
Prerequisites: macOS with Xcode installed
|
||||||
|
|
||||||
1. Build the web assets:
|
#### First-time iOS Configuration
|
||||||
|
|
||||||
|
- Generate certificates inside XCode.
|
||||||
|
|
||||||
|
- Right-click on App and under Signing & Capabilities set the Team.
|
||||||
|
|
||||||
|
#### Each Release
|
||||||
|
|
||||||
|
0. First time (or if dependencies change):
|
||||||
|
|
||||||
|
- `pkgx +rubygems.org sh`
|
||||||
|
|
||||||
|
- ... and you may have to fix these, especially with pkgx:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gem_path=$(which gem)
|
||||||
|
shortened_path="${gem_path:h:h}"
|
||||||
|
export GEM_HOME=$shortened_path
|
||||||
|
export GEM_PATH=$shortened_path
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Build the web assets & update ios:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
rm -rf dist
|
rm -rf dist
|
||||||
npm run build:web
|
npm run build:web
|
||||||
npm run build:capacitor
|
npm run build:capacitor
|
||||||
```
|
|
||||||
|
|
||||||
2. Update iOS project with latest build:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx cap sync ios
|
npx cap sync ios
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -353,15 +359,14 @@ Prerequisites: macOS with Xcode installed
|
|||||||
npx capacitor-assets generate --ios
|
npx capacitor-assets generate --ios
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Bump the version to match Android:
|
4. Bump the version to match Android & package.json:
|
||||||
|
|
||||||
```
|
```
|
||||||
cd ios/App
|
cd ios/App
|
||||||
xcrun agvtool new-version 21
|
xcrun agvtool new-version 34
|
||||||
# Unfortunately this edits Info.plist directly.
|
# Unfortunately this edits Info.plist directly.
|
||||||
#xcrun agvtool new-marketing-version 0.4.5
|
#xcrun agvtool new-marketing-version 0.4.5
|
||||||
cat App.xcodeproj/project.pbxproj | sed "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 0.4.7;/g" > temp
|
cat App.xcodeproj/project.pbxproj | sed "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 0.5.8;/g" > temp && mv temp App.xcodeproj/project.pbxproj
|
||||||
mv temp App.xcodeproj/project.pbxproj
|
|
||||||
cd -
|
cd -
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -377,7 +382,7 @@ Prerequisites: macOS with Xcode installed
|
|||||||
|
|
||||||
7. Release
|
7. Release
|
||||||
|
|
||||||
* Under "General" we want to rename a bunch of things to "Time Safari"
|
* Someday: Under "General" we want to rename a bunch of things to "Time Safari"
|
||||||
* Choose Product -> Destination -> Any iOS Device
|
* Choose Product -> Destination -> Any iOS Device
|
||||||
* Choose Product -> Archive
|
* Choose Product -> Archive
|
||||||
* This will trigger a build and take time, needing user's "login" keychain password (user's login password), repeatedly.
|
* This will trigger a build and take time, needing user's "login" keychain password (user's login password), repeatedly.
|
||||||
@@ -389,15 +394,9 @@ Prerequisites: macOS with Xcode installed
|
|||||||
* You'll probably have to "Manage" something about encryption, disallowed in France.
|
* You'll probably have to "Manage" something about encryption, disallowed in France.
|
||||||
* Then "Save" and "Add to Review" and "Resubmit to App Review".
|
* Then "Save" and "Add to Review" and "Resubmit to App Review".
|
||||||
|
|
||||||
#### First-time iOS Configuration
|
|
||||||
|
|
||||||
- Generate certificates inside XCode.
|
|
||||||
|
|
||||||
- Right-click on App and under Signing & Capabilities set the Team.
|
|
||||||
|
|
||||||
### Android Build
|
### Android Build
|
||||||
|
|
||||||
Prerequisites: Android Studio with SDK installed
|
Prerequisites: Android Studio with Java SDK installed
|
||||||
|
|
||||||
1. Build the web assets:
|
1. Build the web assets:
|
||||||
|
|
||||||
@@ -419,7 +418,7 @@ Prerequisites: Android Studio with SDK installed
|
|||||||
npx capacitor-assets generate --android
|
npx capacitor-assets generate --android
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Bump version to match iOS: android/app/build.gradle
|
4. Bump version to match iOS & package.json: android/app/build.gradle
|
||||||
|
|
||||||
5. Open the project in Android Studio:
|
5. Open the project in Android Studio:
|
||||||
|
|
||||||
@@ -436,7 +435,6 @@ Prerequisites: Android Studio with SDK installed
|
|||||||
./gradlew clean
|
./gradlew clean
|
||||||
./gradlew build -Dlint.baselines.continue=true
|
./gradlew build -Dlint.baselines.continue=true
|
||||||
cd -
|
cd -
|
||||||
npx cap run android
|
|
||||||
```
|
```
|
||||||
|
|
||||||
... or, to create the `aab` file, `bundle` instead of `build`:
|
... or, to create the `aab` file, `bundle` instead of `build`:
|
||||||
@@ -452,7 +450,9 @@ Prerequisites: Android Studio with SDK installed
|
|||||||
* Then `bundleRelease`:
|
* Then `bundleRelease`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
cd android
|
||||||
./gradlew bundleRelease -Dlint.baselines.continue=true
|
./gradlew bundleRelease -Dlint.baselines.continue=true
|
||||||
|
cd -
|
||||||
```
|
```
|
||||||
|
|
||||||
... and find your `aab` file at app/build/outputs/bundle/release
|
... and find your `aab` file at app/build/outputs/bundle/release
|
||||||
@@ -468,7 +468,7 @@ At play.google.com/console:
|
|||||||
- 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.
|
- Note that if you add testers, you have to go to "Publishing Overview" and send those changes or your (closed) testers won't see it.
|
||||||
|
|
||||||
|
|
||||||
## First-time Android Configuration for deep links
|
## Android Configuration for deep links
|
||||||
|
|
||||||
You must add the following intent filter to the `android/app/src/main/AndroidManifest.xml` file:
|
You must add the following intent filter to the `android/app/src/main/AndroidManifest.xml` file:
|
||||||
|
|
||||||
@@ -479,4 +479,6 @@ You must add the following intent filter to the `android/app/src/main/AndroidMan
|
|||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
<data android:scheme="timesafari" />
|
<data android:scheme="timesafari" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
... though when we tried that most recently it failed to 'build' the APK with: http(s) scheme and host attribute are missing, but are required for Android App Links [AppLinkUrlError]
|
||||||
|
|||||||
@@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
|
||||||
|
## [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]
|
## [0.4.7]
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ android {
|
|||||||
applicationId "app.timesafari.app"
|
applicationId "app.timesafari.app"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 23
|
versionCode 34
|
||||||
versionName "0.4.8"
|
versionName "0.5.8"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ios": {
|
"ios": {
|
||||||
"contentInset": "always",
|
"contentInset": "never",
|
||||||
"allowsLinkPreview": true,
|
"allowsLinkPreview": true,
|
||||||
"scrollEnabled": true,
|
"scrollEnabled": true,
|
||||||
"limitsNavigationsToAppBoundDomains": true,
|
"limitsNavigationsToAppBoundDomains": true,
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ try {
|
|||||||
- `src/interfaces/deepLinks.ts`: Type definitions and validation schemas
|
- `src/interfaces/deepLinks.ts`: Type definitions and validation schemas
|
||||||
- `src/services/deepLinks.ts`: Deep link processing service
|
- `src/services/deepLinks.ts`: Deep link processing service
|
||||||
- `src/main.capacitor.ts`: Capacitor integration
|
- `src/main.capacitor.ts`: Capacitor integration
|
||||||
|
- `src/views/DeepLinkRedirectView.vue`: Page to handle links to both mobile and web
|
||||||
|
|
||||||
## Type Safety Examples
|
## Type Safety Examples
|
||||||
|
|
||||||
|
|||||||
@@ -403,7 +403,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 23;
|
CURRENT_PROJECT_VERSION = 34;
|
||||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||||
ENABLE_APP_SANDBOX = NO;
|
ENABLE_APP_SANDBOX = NO;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
@@ -413,7 +413,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.4.8;
|
MARKETING_VERSION = 0.5.8;
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@@ -430,7 +430,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 23;
|
CURRENT_PROJECT_VERSION = 34;
|
||||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||||
ENABLE_APP_SANDBOX = NO;
|
ENABLE_APP_SANDBOX = NO;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
@@ -440,7 +440,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.4.8;
|
MARKETING_VERSION = 0.5.8;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||||
|
|||||||
@@ -49,5 +49,16 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>app.timesafari</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>timesafari</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
1217
package-lock.json
generated
1217
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "timesafari",
|
"name": "timesafari",
|
||||||
"version": "0.4.8",
|
"version": "0.5.8",
|
||||||
"description": "Time Safari Application",
|
"description": "Time Safari Application",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Time Safari Team"
|
"name": "Time Safari Team"
|
||||||
|
|||||||
@@ -49,7 +49,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a class="cursor-pointer" @click="$emit('loadClaim', record.jwtId)" data-testid="circle-info-link">
|
<a
|
||||||
|
class="cursor-pointer"
|
||||||
|
data-testid="circle-info-link"
|
||||||
|
@click="$emit('loadClaim', record.jwtId)"
|
||||||
|
>
|
||||||
<font-awesome icon="circle-info" class="fa-fw text-slate-500" />
|
<font-awesome icon="circle-info" class="fa-fw text-slate-500" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -104,7 +104,6 @@ import { USE_DEXIE_DB } from "@/constants/app";
|
|||||||
import * as databaseUtil from "../db/databaseUtil";
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
||||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
@@ -143,19 +142,23 @@ export default class FeedFilters extends Vue {
|
|||||||
async toggleHasVisibleDid() {
|
async toggleHasVisibleDid() {
|
||||||
this.settingChanged = true;
|
this.settingChanged = true;
|
||||||
this.hasVisibleDid = !this.hasVisibleDid;
|
this.hasVisibleDid = !this.hasVisibleDid;
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
await databaseUtil.updateDefaultSettings({
|
||||||
filterFeedByVisible: this.hasVisibleDid,
|
filterFeedByVisible: this.hasVisibleDid,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (USE_DEXIE_DB) {
|
||||||
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
filterFeedByVisible: this.hasVisibleDid,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async toggleNearby() {
|
async toggleNearby() {
|
||||||
this.settingChanged = true;
|
this.settingChanged = true;
|
||||||
this.isNearby = !this.isNearby;
|
this.isNearby = !this.isNearby;
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
await databaseUtil.updateDefaultSettings({
|
||||||
await platformService.dbExec(
|
filterFeedByNearby: this.isNearby,
|
||||||
`UPDATE settings SET filterFeedByNearby = ? WHERE id = ?`,
|
});
|
||||||
[this.isNearby, MASTER_SETTINGS_KEY],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
@@ -169,11 +172,10 @@ export default class FeedFilters extends Vue {
|
|||||||
this.settingChanged = true;
|
this.settingChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
await databaseUtil.updateDefaultSettings({
|
||||||
await platformService.dbExec(
|
filterFeedByNearby: false,
|
||||||
`UPDATE settings SET filterFeedByNearby = ? AND filterFeedByVisible = ? WHERE id = ?`,
|
filterFeedByVisible: false,
|
||||||
[false, false, MASTER_SETTINGS_KEY],
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
@@ -191,11 +193,10 @@ export default class FeedFilters extends Vue {
|
|||||||
this.settingChanged = true;
|
this.settingChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
await databaseUtil.updateDefaultSettings({
|
||||||
await platformService.dbExec(
|
filterFeedByNearby: true,
|
||||||
`UPDATE settings SET filterFeedByNearby = ? AND filterFeedByVisible = ? WHERE id = ?`,
|
filterFeedByVisible: true,
|
||||||
[true, true, MASTER_SETTINGS_KEY],
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
|
|||||||
@@ -321,7 +321,7 @@ export default class GiftedDialog extends Vue {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
const errorMessage = this.getGiveCreationErrorMessage(result);
|
const errorMessage = result.error;
|
||||||
logger.error("Error with give creation result:", result);
|
logger.error("Error with give creation result:", result);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
@@ -367,19 +367,6 @@ export default class GiftedDialog extends Vue {
|
|||||||
|
|
||||||
// Helper functions for readability
|
// Helper functions for readability
|
||||||
|
|
||||||
/**
|
|
||||||
* @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() {
|
explainData() {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -227,6 +227,7 @@ export default class GivenPrompts extends Vue {
|
|||||||
|
|
||||||
let someContactDbIndex = Math.floor(Math.random() * this.numContacts);
|
let someContactDbIndex = Math.floor(Math.random() * this.numContacts);
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
// as long as the index has an entry, loop
|
// as long as the index has an entry, loop
|
||||||
while (
|
while (
|
||||||
this.shownContactDbIndices[someContactDbIndex] != null &&
|
this.shownContactDbIndices[someContactDbIndex] != null &&
|
||||||
@@ -245,9 +246,8 @@ export default class GivenPrompts extends Vue {
|
|||||||
[someContactDbIndex],
|
[someContactDbIndex],
|
||||||
);
|
);
|
||||||
if (result) {
|
if (result) {
|
||||||
this.currentContact = databaseUtil.mapQueryResultToValues(result)[
|
const mappedContacts = databaseUtil.mapQueryResultToValues(result);
|
||||||
someContactDbIndex
|
this.currentContact = mappedContacts[0] as unknown as Contact;
|
||||||
] as unknown as Contact;
|
|
||||||
}
|
}
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
await db.open();
|
await db.open();
|
||||||
|
|||||||
@@ -48,15 +48,15 @@
|
|||||||
<span>
|
<span>
|
||||||
{{ didInfo(visDid) }}
|
{{ didInfo(visDid) }}
|
||||||
<span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)">
|
<span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)">
|
||||||
<a
|
<router-link
|
||||||
:href="`/did/${visDid}`"
|
:to="{ path: '/did/' + encodeURIComponent(visDid) }"
|
||||||
class="text-blue-500"
|
class="text-blue-500"
|
||||||
>
|
>
|
||||||
<font-awesome
|
<font-awesome
|
||||||
icon="arrow-up-right-from-square"
|
icon="arrow-up-right-from-square"
|
||||||
class="fa-fw"
|
class="fa-fw"
|
||||||
/>
|
/>
|
||||||
</a>
|
</router-link>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
If you'd like an introduction,
|
If you'd like an introduction,
|
||||||
<a
|
<a
|
||||||
class="text-blue-500"
|
class="text-blue-500"
|
||||||
@click="copyToClipboard('A link to this page', windowLocation)"
|
@click="copyToClipboard('A link to this page', deepLinkUrl)"
|
||||||
>click here to copy this page, paste it into a message, and ask if
|
>click here to copy this page, paste it into a message, and ask if
|
||||||
they'll tell you more about the {{ roleName }}.</a
|
they'll tell you more about the {{ roleName }}.</a
|
||||||
>
|
>
|
||||||
@@ -104,7 +104,7 @@ import * as R from "ramda";
|
|||||||
import { useClipboard } from "@vueuse/core";
|
import { useClipboard } from "@vueuse/core";
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from "../db/tables/contacts";
|
||||||
import * as serverUtil from "../libs/endorserServer";
|
import * as serverUtil from "../libs/endorserServer";
|
||||||
import { NotificationIface } from "../constants/app";
|
import { APP_SERVER, NotificationIface } from "../constants/app";
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class HiddenDidDialog extends Vue {
|
export default class HiddenDidDialog extends Vue {
|
||||||
@@ -117,7 +117,8 @@ export default class HiddenDidDialog extends Vue {
|
|||||||
activeDid = "";
|
activeDid = "";
|
||||||
allMyDids: Array<string> = [];
|
allMyDids: Array<string> = [];
|
||||||
canShare = false;
|
canShare = false;
|
||||||
windowLocation = window.location.href;
|
deepLinkPathSuffix = "";
|
||||||
|
deepLinkUrl = window.location.href; // this is changed to a deep link in the setup
|
||||||
|
|
||||||
R = R;
|
R = R;
|
||||||
serverUtil = serverUtil;
|
serverUtil = serverUtil;
|
||||||
@@ -129,17 +130,21 @@ export default class HiddenDidDialog extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
open(
|
open(
|
||||||
|
deepLinkPathSuffix: string,
|
||||||
roleName: string,
|
roleName: string,
|
||||||
visibleToDids: string[],
|
visibleToDids: string[],
|
||||||
allContacts: Array<Contact>,
|
allContacts: Array<Contact>,
|
||||||
activeDid: string,
|
activeDid: string,
|
||||||
allMyDids: Array<string>,
|
allMyDids: Array<string>,
|
||||||
) {
|
) {
|
||||||
|
this.deepLinkPathSuffix = deepLinkPathSuffix;
|
||||||
this.roleName = roleName;
|
this.roleName = roleName;
|
||||||
this.visibleToDids = visibleToDids;
|
this.visibleToDids = visibleToDids;
|
||||||
this.allContacts = allContacts;
|
this.allContacts = allContacts;
|
||||||
this.activeDid = activeDid;
|
this.activeDid = activeDid;
|
||||||
this.allMyDids = allMyDids;
|
this.allMyDids = allMyDids;
|
||||||
|
|
||||||
|
this.deepLinkUrl = APP_SERVER + "/deep-link/" + this.deepLinkPathSuffix;
|
||||||
this.isOpen = true;
|
this.isOpen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,11 +178,11 @@ export default class HiddenDidDialog extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onClickShareClaim() {
|
onClickShareClaim() {
|
||||||
this.copyToClipboard("A link to this page", this.windowLocation);
|
this.copyToClipboard("A link to this page", this.deepLinkUrl);
|
||||||
window.navigator.share({
|
window.navigator.share({
|
||||||
title: "Help Connect Me",
|
title: "Help Connect Me",
|
||||||
text: "I'm trying to find the people who recorded this. Can you help me?",
|
text: "I'm trying to find the people who recorded this. Can you help me?",
|
||||||
url: this.windowLocation,
|
url: this.deepLinkUrl,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,10 +83,7 @@
|
|||||||
import { Vue, Component, Prop } from "vue-facing-decorator";
|
import { Vue, Component, Prop } from "vue-facing-decorator";
|
||||||
|
|
||||||
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||||
import {
|
import { createAndSubmitOffer } from "../libs/endorserServer";
|
||||||
createAndSubmitOffer,
|
|
||||||
serverMessageForUser,
|
|
||||||
} from "../libs/endorserServer";
|
|
||||||
import * as libsUtil from "../libs/util";
|
import * as libsUtil from "../libs/util";
|
||||||
import * as databaseUtil from "../db/databaseUtil";
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
import { retrieveSettingsForActiveAccount } from "../db/index";
|
import { retrieveSettingsForActiveAccount } from "../db/index";
|
||||||
@@ -250,7 +247,7 @@ export default class OfferDialog extends Vue {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
const errorMessage = this.getOfferCreationErrorMessage(result);
|
const errorMessage = result.error;
|
||||||
logger.error("Error with offer creation result:", result);
|
logger.error("Error with offer creation result:", result);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
@@ -290,21 +287,6 @@ export default class OfferDialog extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions for readability
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
|
|
||||||
* @returns best guess at an error message
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
getOfferCreationErrorMessage(result: any) {
|
|
||||||
return (
|
|
||||||
serverMessageForUser(result) ||
|
|
||||||
result.error?.userMessage ||
|
|
||||||
result.error?.error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
The feed underneath this pop-up shows the latest contributions,
|
The feed underneath this pop-up shows the latest contributions, some from
|
||||||
some from people and some from projects.
|
people and some from projects.
|
||||||
|
|
||||||
<p v-if="isRegistered" class="mt-4">
|
<p v-if="isRegistered" class="mt-4">
|
||||||
You can now log things that you've seen:
|
You can now log things that you've seen:
|
||||||
@@ -29,8 +29,7 @@
|
|||||||
button to express your appreciation for... whatever.
|
button to express your appreciation for... whatever.
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-4">
|
<p class="mt-4">
|
||||||
Once someone registers you, you can log your
|
Once someone registers you, you can log your appreciation, too.
|
||||||
appreciation, too.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="mt-4">
|
<p class="mt-4">
|
||||||
@@ -260,7 +259,7 @@ export default class OnboardingDialog extends Vue {
|
|||||||
this.visible = true;
|
this.visible = true;
|
||||||
if (this.page === OnboardPage.Create) {
|
if (this.page === OnboardPage.Create) {
|
||||||
// we'll assume that they've been through all the other pages
|
// we'll assume that they've been through all the other pages
|
||||||
await databaseUtil.updateAccountSettings(this.activeDid, {
|
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
|
||||||
finishedOnboarding: true,
|
finishedOnboarding: true,
|
||||||
});
|
});
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
@@ -274,7 +273,7 @@ export default class OnboardingDialog extends Vue {
|
|||||||
async onClickClose(done?: boolean, goHome?: boolean) {
|
async onClickClose(done?: boolean, goHome?: boolean) {
|
||||||
this.visible = false;
|
this.visible = false;
|
||||||
if (done) {
|
if (done) {
|
||||||
await databaseUtil.updateAccountSettings(this.activeDid, {
|
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
|
||||||
finishedOnboarding: true,
|
finishedOnboarding: true,
|
||||||
});
|
});
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
|
|||||||
@@ -8,11 +8,7 @@
|
|||||||
>
|
>
|
||||||
<div class="h-full w-full object-contain" v-html="generateIcon()" />
|
<div class="h-full w-full object-contain" v-html="generateIcon()" />
|
||||||
</a>
|
</a>
|
||||||
<div
|
<div v-else class="h-full w-full object-contain" v-html="generateIcon()" />
|
||||||
v-else
|
|
||||||
class="h-full w-full object-contain"
|
|
||||||
v-html="generateIcon()"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { toSvg } from "jdenticon";
|
import { toSvg } from "jdenticon";
|
||||||
|
|||||||
@@ -38,14 +38,13 @@ export default class TopMessage extends Vue {
|
|||||||
settings.apiServer !== AppString.PROD_ENDORSER_API_SERVER
|
settings.apiServer !== AppString.PROD_ENDORSER_API_SERVER
|
||||||
) {
|
) {
|
||||||
const didPrefix = settings.activeDid?.slice(11, 15);
|
const didPrefix = settings.activeDid?.slice(11, 15);
|
||||||
this.message = "You're linked to a non-prod server, user " + didPrefix;
|
this.message = "You're not using prod, user " + didPrefix;
|
||||||
} else if (
|
} else if (
|
||||||
settings.warnIfProdServer &&
|
settings.warnIfProdServer &&
|
||||||
settings.apiServer === AppString.PROD_ENDORSER_API_SERVER
|
settings.apiServer === AppString.PROD_ENDORSER_API_SERVER
|
||||||
) {
|
) {
|
||||||
const didPrefix = settings.activeDid?.slice(11, 15);
|
const didPrefix = settings.activeDid?.slice(11, 15);
|
||||||
this.message =
|
this.message = "You are using prod, user " + didPrefix;
|
||||||
"You're linked to the production server, user " + didPrefix;
|
|
||||||
}
|
}
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
|
|||||||
@@ -33,18 +33,18 @@ export const APP_SERVER =
|
|||||||
|
|
||||||
export const DEFAULT_ENDORSER_API_SERVER =
|
export const DEFAULT_ENDORSER_API_SERVER =
|
||||||
import.meta.env.VITE_DEFAULT_ENDORSER_API_SERVER ||
|
import.meta.env.VITE_DEFAULT_ENDORSER_API_SERVER ||
|
||||||
AppString.TEST_ENDORSER_API_SERVER;
|
AppString.PROD_ENDORSER_API_SERVER;
|
||||||
|
|
||||||
export const DEFAULT_IMAGE_API_SERVER =
|
export const DEFAULT_IMAGE_API_SERVER =
|
||||||
import.meta.env.VITE_DEFAULT_IMAGE_API_SERVER ||
|
import.meta.env.VITE_DEFAULT_IMAGE_API_SERVER ||
|
||||||
AppString.TEST_IMAGE_API_SERVER;
|
AppString.PROD_IMAGE_API_SERVER;
|
||||||
|
|
||||||
export const DEFAULT_PARTNER_API_SERVER =
|
export const DEFAULT_PARTNER_API_SERVER =
|
||||||
import.meta.env.VITE_DEFAULT_PARTNER_API_SERVER ||
|
import.meta.env.VITE_DEFAULT_PARTNER_API_SERVER ||
|
||||||
AppString.TEST_PARTNER_API_SERVER;
|
AppString.PROD_PARTNER_API_SERVER;
|
||||||
|
|
||||||
export const DEFAULT_PUSH_SERVER =
|
export const DEFAULT_PUSH_SERVER =
|
||||||
import.meta.env.VITE_DEFAULT_PUSH_SERVER || "https://timesafari.app";
|
import.meta.env.VITE_DEFAULT_PUSH_SERVER || AppString.PROD_PUSH_SERVER;
|
||||||
|
|
||||||
export const IMAGE_TYPE_PROFILE = "profile";
|
export const IMAGE_TYPE_PROFILE = "profile";
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,20 @@ export async function updateDefaultSettings(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateAccountSettings(
|
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,
|
accountDid: string,
|
||||||
settingsChanges: Settings,
|
settingsChanges: Settings,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
@@ -55,20 +68,7 @@ export async function updateAccountSettings(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const updateResult = await platform.dbExec(updateSql, updateParams);
|
const updateResult = await platform.dbExec(updateSql, updateParams);
|
||||||
|
return updateResult.changes === 1;
|
||||||
// If no record was updated, insert a new one
|
|
||||||
if (updateResult.changes === 1) {
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
const columns = Object.keys(settingsChanges);
|
|
||||||
const values = Object.values(settingsChanges);
|
|
||||||
const placeholders = values.map(() => "?").join(", ");
|
|
||||||
|
|
||||||
const insertSql = `INSERT INTO settings (${columns.join(", ")}) VALUES (${placeholders})`;
|
|
||||||
const result = await platform.dbExec(insertSql, values);
|
|
||||||
|
|
||||||
return result.changes === 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_SETTINGS: Settings = {
|
const DEFAULT_SETTINGS: Settings = {
|
||||||
@@ -109,9 +109,6 @@ export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
|
|||||||
const defaultSettings = await retrieveSettingsForDefaultAccount();
|
const defaultSettings = await retrieveSettingsForDefaultAccount();
|
||||||
// If no active DID, return defaults
|
// If no active DID, return defaults
|
||||||
if (!defaultSettings.activeDid) {
|
if (!defaultSettings.activeDid) {
|
||||||
logConsoleAndDb(
|
|
||||||
"[databaseUtil] No active DID found, returning default settings",
|
|
||||||
);
|
|
||||||
return defaultSettings;
|
return defaultSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,9 +121,7 @@ export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!result?.values?.length) {
|
if (!result?.values?.length) {
|
||||||
logConsoleAndDb(
|
// we created DID-specific settings when generated or imported, so this shouldn't happen
|
||||||
`[databaseUtil] No account-specific settings found for ${defaultSettings.activeDid}`,
|
|
||||||
);
|
|
||||||
return defaultSettings;
|
return defaultSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,6 +130,7 @@ export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
|
|||||||
result.columns,
|
result.columns,
|
||||||
result.values,
|
result.values,
|
||||||
)[0] as Settings;
|
)[0] as Settings;
|
||||||
|
|
||||||
const overrideSettingsFiltered = Object.fromEntries(
|
const overrideSettingsFiltered = Object.fromEntries(
|
||||||
Object.entries(overrideSettings).filter(([_, v]) => v !== null),
|
Object.entries(overrideSettings).filter(([_, v]) => v !== null),
|
||||||
);
|
);
|
||||||
@@ -144,17 +140,7 @@ export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
|
|||||||
|
|
||||||
// Handle searchBoxes parsing
|
// Handle searchBoxes parsing
|
||||||
if (settings.searchBoxes) {
|
if (settings.searchBoxes) {
|
||||||
try {
|
settings.searchBoxes = parseJsonField(settings.searchBoxes, []);
|
||||||
// @ts-expect-error - the searchBoxes field is a string in the DB
|
|
||||||
settings.searchBoxes = JSON.parse(settings.searchBoxes);
|
|
||||||
} catch (error) {
|
|
||||||
logConsoleAndDb(
|
|
||||||
`[databaseUtil] Failed to parse searchBoxes for ${defaultSettings.activeDid}: ${error}`,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
// Reset to empty array on parse failure
|
|
||||||
settings.searchBoxes = [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return settings;
|
return settings;
|
||||||
@@ -233,9 +219,9 @@ export async function logConsoleAndDb(
|
|||||||
isError = false,
|
isError = false,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (isError) {
|
if (isError) {
|
||||||
logger.error(`${new Date().toISOString()} ${message}`);
|
logger.error(`${new Date().toISOString()}`, message);
|
||||||
} else {
|
} else {
|
||||||
logger.log(`${new Date().toISOString()} ${message}`);
|
logger.log(`${new Date().toISOString()}`, message);
|
||||||
}
|
}
|
||||||
await logToDb(message);
|
await logToDb(message);
|
||||||
}
|
}
|
||||||
@@ -254,6 +240,7 @@ export function generateInsertStatement(
|
|||||||
const values = Object.values(model).filter((value) => value !== undefined);
|
const values = Object.values(model).filter((value) => value !== undefined);
|
||||||
const placeholders = values.map(() => "?").join(", ");
|
const placeholders = values.map(() => "?").join(", ");
|
||||||
const insertSql = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${placeholders})`;
|
const insertSql = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${placeholders})`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sql: insertSql,
|
sql: insertSql,
|
||||||
params: values,
|
params: values,
|
||||||
@@ -325,3 +312,115 @@ export function mapColumnsToValues(
|
|||||||
return obj;
|
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,6 +1,4 @@
|
|||||||
import { AxiosResponse } from "axios";
|
|
||||||
import { GiverReceiverInputInfo } from "../libs/util";
|
import { GiverReceiverInputInfo } from "../libs/util";
|
||||||
import { ErrorResult, ResultWithType } from "./common";
|
|
||||||
|
|
||||||
export interface GiverOutputInfo {
|
export interface GiverOutputInfo {
|
||||||
action: string;
|
action: string;
|
||||||
@@ -47,12 +45,3 @@ export interface ProviderInfo {
|
|||||||
*/
|
*/
|
||||||
linkConfirmed: boolean;
|
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>;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -15,10 +15,6 @@ export interface GenericCredWrapper<T extends GenericVerifiableCredential> {
|
|||||||
publicUrls?: Record<string, string>;
|
publicUrls?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ResultWithType {
|
|
||||||
type: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ErrorResponse {
|
export interface ErrorResponse {
|
||||||
error?: {
|
error?: {
|
||||||
message?: string;
|
message?: string;
|
||||||
@@ -30,11 +26,6 @@ export interface InternalError {
|
|||||||
userMessage?: string;
|
userMessage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ErrorResult extends ResultWithType {
|
|
||||||
type: "error";
|
|
||||||
error: InternalError;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface KeyMeta {
|
export interface KeyMeta {
|
||||||
did: string;
|
did: string;
|
||||||
publicKeyHex: string;
|
publicKeyHex: string;
|
||||||
|
|||||||
@@ -29,18 +29,17 @@ import { z } from "zod";
|
|||||||
|
|
||||||
// Add a union type of all valid route paths
|
// Add a union type of all valid route paths
|
||||||
export const VALID_DEEP_LINK_ROUTES = [
|
export const VALID_DEEP_LINK_ROUTES = [
|
||||||
"user-profile",
|
// note that similar lists are below in deepLinkSchemas and in src/services/deepLinks.ts
|
||||||
"project-details",
|
|
||||||
"onboard-meeting-setup",
|
|
||||||
"invite-one-accept",
|
|
||||||
"contact-import",
|
|
||||||
"confirm-gift",
|
|
||||||
"claim",
|
"claim",
|
||||||
"claim-cert",
|
|
||||||
"claim-add-raw",
|
"claim-add-raw",
|
||||||
"contact-edit",
|
"claim-cert",
|
||||||
"contacts",
|
"confirm-gift",
|
||||||
|
"contact-import",
|
||||||
"did",
|
"did",
|
||||||
|
"invite-one-accept",
|
||||||
|
"onboard-meeting-setup",
|
||||||
|
"project",
|
||||||
|
"user-profile",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
// Create a type from the array
|
// 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
|
// Parameter validation schemas for each route type
|
||||||
export const deepLinkSchemas = {
|
export const deepLinkSchemas = {
|
||||||
"user-profile": z.object({
|
// note that similar lists are above in VALID_DEEP_LINK_ROUTES and in src/services/deepLinks.ts
|
||||||
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(),
|
|
||||||
}),
|
|
||||||
claim: z.object({
|
claim: z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
}),
|
}),
|
||||||
"claim-cert": z.object({
|
|
||||||
id: z.string(),
|
|
||||||
}),
|
|
||||||
"claim-add-raw": z.object({
|
"claim-add-raw": z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
claim: z.string().optional(),
|
claim: z.string().optional(),
|
||||||
claimJwtId: z.string().optional(),
|
claimJwtId: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
"contact-edit": z.object({
|
"claim-cert": z.object({
|
||||||
did: z.string(),
|
id: z.string(),
|
||||||
}),
|
}),
|
||||||
contacts: z.object({
|
"confirm-gift": z.object({
|
||||||
contacts: z.string(), // JSON string of contacts array
|
id: z.string(),
|
||||||
|
}),
|
||||||
|
"contact-import": z.object({
|
||||||
|
jwt: z.string(),
|
||||||
}),
|
}),
|
||||||
did: z.object({
|
did: z.object({
|
||||||
did: z.string(),
|
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 = {
|
export type DeepLinkParams = {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export type {
|
export type {
|
||||||
// From common.ts
|
// From common.ts
|
||||||
|
CreateAndSubmitClaimResult,
|
||||||
GenericCredWrapper,
|
GenericCredWrapper,
|
||||||
GenericVerifiableCredential,
|
GenericVerifiableCredential,
|
||||||
KeyMeta,
|
KeyMeta,
|
||||||
@@ -18,11 +19,6 @@ export type {
|
|||||||
RegisterActionClaim,
|
RegisterActionClaim,
|
||||||
} from "./claims";
|
} from "./claims";
|
||||||
|
|
||||||
export type {
|
|
||||||
// From claims-result.ts
|
|
||||||
CreateAndSubmitClaimResult,
|
|
||||||
} from "./claims-result";
|
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
// From records.ts
|
// From records.ts
|
||||||
PlanSummaryRecord,
|
PlanSummaryRecord,
|
||||||
|
|||||||
@@ -979,7 +979,7 @@ export const createAndSubmitConfirmation = async (
|
|||||||
handleId: string | undefined,
|
handleId: string | undefined,
|
||||||
apiServer: string,
|
apiServer: string,
|
||||||
axios: Axios,
|
axios: Axios,
|
||||||
) => {
|
): Promise<CreateAndSubmitClaimResult> => {
|
||||||
const goodClaim = removeSchemaContext(
|
const goodClaim = removeSchemaContext(
|
||||||
removeVisibleToDids(
|
removeVisibleToDids(
|
||||||
addLastClaimOrHandleAsIdIfMissing(claim, lastClaimId, handleId),
|
addLastClaimOrHandleAsIdIfMissing(claim, lastClaimId, handleId),
|
||||||
@@ -1074,7 +1074,8 @@ export async function generateEndorserJwtUrlForAccount(
|
|||||||
|
|
||||||
const vcJwt = await createEndorserJwtForDid(account.did, contactInfo);
|
const vcJwt = await createEndorserJwtForDid(account.did, contactInfo);
|
||||||
|
|
||||||
const viewPrefix = APP_SERVER + CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI;
|
const viewPrefix =
|
||||||
|
APP_SERVER + "/deep-link" + CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI;
|
||||||
return viewPrefix + vcJwt;
|
return viewPrefix + vcJwt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import { logger } from "../utils/logger";
|
|||||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||||
import { sha256 } from "ethereum-cryptography/sha256";
|
import { sha256 } from "ethereum-cryptography/sha256";
|
||||||
import { IIdentifier } from "@veramo/core";
|
import { IIdentifier } from "@veramo/core";
|
||||||
|
import { insertDidSpecificSettings, parseJsonField } from "../db/databaseUtil";
|
||||||
|
|
||||||
export interface GiverReceiverInputInfo {
|
export interface GiverReceiverInputInfo {
|
||||||
did?: string;
|
did?: string;
|
||||||
@@ -626,7 +627,9 @@ export const retrieveFullyDecryptedAccount = async (
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const retrieveAllAccountsMetadata = async (): Promise<AccountEncrypted[]> => {
|
export const retrieveAllAccountsMetadata = async (): Promise<
|
||||||
|
AccountEncrypted[]
|
||||||
|
> => {
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
const dbAccounts = await platformService.dbQuery(`SELECT * FROM accounts`);
|
const dbAccounts = await platformService.dbQuery(`SELECT * FROM accounts`);
|
||||||
const accounts = databaseUtil.mapQueryResultToValues(dbAccounts) as Account[];
|
const accounts = databaseUtil.mapQueryResultToValues(dbAccounts) as Account[];
|
||||||
@@ -643,8 +646,12 @@ export const retrieveAllAccountsMetadata = async (): Promise<AccountEncrypted[]>
|
|||||||
// This is not accurate because they can't be decrypted, but we're removing Dexie anyway.
|
// This is not accurate because they can't be decrypted, but we're removing Dexie anyway.
|
||||||
const identityStr = JSON.stringify(identity);
|
const identityStr = JSON.stringify(identity);
|
||||||
const encryptedAccount = {
|
const encryptedAccount = {
|
||||||
identityEncrBase64: sha256(new TextEncoder().encode(identityStr)).toString(),
|
identityEncrBase64: sha256(
|
||||||
mnemonicEncrBase64: sha256(new TextEncoder().encode(account.mnemonic)).toString(),
|
new TextEncoder().encode(identityStr),
|
||||||
|
).toString(),
|
||||||
|
mnemonicEncrBase64: sha256(
|
||||||
|
new TextEncoder().encode(account.mnemonic),
|
||||||
|
).toString(),
|
||||||
...metadata,
|
...metadata,
|
||||||
};
|
};
|
||||||
return encryptedAccount as AccountEncrypted;
|
return encryptedAccount as AccountEncrypted;
|
||||||
@@ -691,6 +698,7 @@ export async function saveNewIdentity(
|
|||||||
];
|
];
|
||||||
await platformService.dbExec(sql, params);
|
await platformService.dbExec(sql, params);
|
||||||
await databaseUtil.updateDefaultSettings({ activeDid: identity.did });
|
await databaseUtil.updateDefaultSettings({ activeDid: identity.did });
|
||||||
|
await databaseUtil.insertDidSpecificSettings(identity.did);
|
||||||
|
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||||
@@ -704,6 +712,7 @@ export async function saveNewIdentity(
|
|||||||
publicKeyHex: identity.keys[0].publicKeyHex,
|
publicKeyHex: identity.keys[0].publicKeyHex,
|
||||||
});
|
});
|
||||||
await updateDefaultSettings({ activeDid: identity.did });
|
await updateDefaultSettings({ activeDid: identity.did });
|
||||||
|
await insertDidSpecificSettings(identity.did);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Failed to update default settings:", error);
|
logger.error("Failed to update default settings:", error);
|
||||||
@@ -726,7 +735,9 @@ export const generateSaveAndActivateIdentity = async (): Promise<string> => {
|
|||||||
const newId = newIdentifier(address, publicHex, privateHex, derivationPath);
|
const newId = newIdentifier(address, publicHex, privateHex, derivationPath);
|
||||||
|
|
||||||
await saveNewIdentity(newId, mnemonic, derivationPath);
|
await saveNewIdentity(newId, mnemonic, derivationPath);
|
||||||
await databaseUtil.updateAccountSettings(newId.did, { isRegistered: false });
|
await databaseUtil.updateDidSpecificSettings(newId.did, {
|
||||||
|
isRegistered: false,
|
||||||
|
});
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
await updateAccountSettings(newId.did, { isRegistered: false });
|
await updateAccountSettings(newId.did, { isRegistered: false });
|
||||||
}
|
}
|
||||||
@@ -768,7 +779,7 @@ export const registerSaveAndActivatePasskey = async (
|
|||||||
): Promise<Account> => {
|
): Promise<Account> => {
|
||||||
const account = await registerAndSavePasskey(keyName);
|
const account = await registerAndSavePasskey(keyName);
|
||||||
await databaseUtil.updateDefaultSettings({ activeDid: account.did });
|
await databaseUtil.updateDefaultSettings({ activeDid: account.did });
|
||||||
await databaseUtil.updateAccountSettings(account.did, {
|
await databaseUtil.updateDidSpecificSettings(account.did, {
|
||||||
isRegistered: false,
|
isRegistered: false,
|
||||||
});
|
});
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
@@ -856,7 +867,7 @@ export const contactToCsvLine = (contact: Contact): string => {
|
|||||||
|
|
||||||
// Handle contactMethods array by stringifying it
|
// Handle contactMethods array by stringifying it
|
||||||
const contactMethodsStr = contact.contactMethods
|
const contactMethodsStr = contact.contactMethods
|
||||||
? escapeField(JSON.stringify(contact.contactMethods))
|
? escapeField(JSON.stringify(parseJsonField(contact.contactMethods, [])))
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
const fields = [
|
const fields = [
|
||||||
@@ -871,6 +882,71 @@ export const contactToCsvLine = (contact: Contact): string => {
|
|||||||
return fields.join(",");
|
return fields.join(",");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a CSV line into a Contact object. See contactToCsvLine for the format.
|
||||||
|
* @param lineRaw - The CSV line to parse
|
||||||
|
* @returns A Contact object
|
||||||
|
*/
|
||||||
|
export const csvLineToContact = (lineRaw: string): Contact => {
|
||||||
|
// Note that Endorser Mobile puts name first, then did, etc.
|
||||||
|
let line = lineRaw.trim();
|
||||||
|
let did, publicKeyInput, seesMe, registered;
|
||||||
|
let name;
|
||||||
|
let commaPos1 = -1;
|
||||||
|
if (line.startsWith('"')) {
|
||||||
|
let doubleDoubleQuotePos = line.lastIndexOf('""') + 2;
|
||||||
|
if (doubleDoubleQuotePos === -1) {
|
||||||
|
doubleDoubleQuotePos = 1;
|
||||||
|
}
|
||||||
|
const quote2Pos = line.indexOf('"', doubleDoubleQuotePos);
|
||||||
|
if (quote2Pos > -1) {
|
||||||
|
commaPos1 = line.indexOf(",", quote2Pos);
|
||||||
|
name = line.substring(1, quote2Pos).trim();
|
||||||
|
name = name.replace(/""/g, '"');
|
||||||
|
} else {
|
||||||
|
// something is weird with one " to start, so ignore it and start after "
|
||||||
|
line = line.substring(1);
|
||||||
|
commaPos1 = line.indexOf(",");
|
||||||
|
name = line.substring(0, commaPos1).trim();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
commaPos1 = line.indexOf(",");
|
||||||
|
name = line.substring(0, commaPos1).trim();
|
||||||
|
}
|
||||||
|
if (commaPos1 > -1) {
|
||||||
|
did = line.substring(commaPos1 + 1).trim();
|
||||||
|
const commaPos2 = line.indexOf(",", commaPos1 + 1);
|
||||||
|
if (commaPos2 > -1) {
|
||||||
|
did = line.substring(commaPos1 + 1, commaPos2).trim();
|
||||||
|
publicKeyInput = line.substring(commaPos2 + 1).trim();
|
||||||
|
const commaPos3 = line.indexOf(",", commaPos2 + 1);
|
||||||
|
if (commaPos3 > -1) {
|
||||||
|
publicKeyInput = line.substring(commaPos2 + 1, commaPos3).trim();
|
||||||
|
seesMe = line.substring(commaPos3 + 1).trim() == "true";
|
||||||
|
const commaPos4 = line.indexOf(",", commaPos3 + 1);
|
||||||
|
if (commaPos4 > -1) {
|
||||||
|
seesMe = line.substring(commaPos3 + 1, commaPos4).trim() == "true";
|
||||||
|
registered = line.substring(commaPos4 + 1).trim() == "true";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// help with potential mistakes while this sharing requires copy-and-paste
|
||||||
|
let publicKeyBase64 = publicKeyInput;
|
||||||
|
if (publicKeyBase64 && /^[0-9A-Fa-f]{66}$/i.test(publicKeyBase64)) {
|
||||||
|
// it must be all hex (compressed public key), so convert
|
||||||
|
publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64");
|
||||||
|
}
|
||||||
|
const newContact: Contact = {
|
||||||
|
did: did || "",
|
||||||
|
name,
|
||||||
|
publicKeyBase64,
|
||||||
|
seesMe,
|
||||||
|
registered,
|
||||||
|
};
|
||||||
|
return newContact;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for the JSON export format of database tables
|
* Interface for the JSON export format of database tables
|
||||||
*/
|
*/
|
||||||
@@ -901,7 +977,7 @@ export const contactsToExportJson = (contacts: Contact[]): DatabaseExport => {
|
|||||||
did: contact.did,
|
did: contact.did,
|
||||||
name: contact.name || null,
|
name: contact.name || null,
|
||||||
contactMethods: contact.contactMethods
|
contactMethods: contact.contactMethods
|
||||||
? JSON.stringify(contact.contactMethods)
|
? JSON.stringify(parseJsonField(contact.contactMethods, []))
|
||||||
: null,
|
: null,
|
||||||
nextPubKeyHashB64: contact.nextPubKeyHashB64 || null,
|
nextPubKeyHashB64: contact.nextPubKeyHashB64 || null,
|
||||||
notes: contact.notes || null,
|
notes: contact.notes || null,
|
||||||
|
|||||||
@@ -34,8 +34,7 @@ import router from "./router";
|
|||||||
import { handleApiError } from "./services/api";
|
import { handleApiError } from "./services/api";
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import { DeepLinkHandler } from "./services/deepLinks";
|
import { DeepLinkHandler } from "./services/deepLinks";
|
||||||
import { logConsoleAndDb } from "./db/databaseUtil";
|
import { logger, safeStringify } from "./utils/logger";
|
||||||
import { logger } from "./utils/logger";
|
|
||||||
|
|
||||||
logger.log("[Capacitor] Starting initialization");
|
logger.log("[Capacitor] Starting initialization");
|
||||||
logger.log("[Capacitor] Platform:", process.env.VITE_PLATFORM);
|
logger.log("[Capacitor] Platform:", process.env.VITE_PLATFORM);
|
||||||
@@ -72,10 +71,10 @@ const handleDeepLink = async (data: { url: string }) => {
|
|||||||
await router.isReady();
|
await router.isReady();
|
||||||
await deepLinkHandler.handleDeepLink(data.url);
|
await deepLinkHandler.handleDeepLink(data.url);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb("[DeepLink] Error handling deep link: " + error, true);
|
logger.error("[DeepLink] Error handling deep link: ", error);
|
||||||
handleApiError(
|
handleApiError(
|
||||||
{
|
{
|
||||||
message: error instanceof Error ? error.message : String(error),
|
message: error instanceof Error ? error.message : safeStringify(error),
|
||||||
} as AxiosError,
|
} as AxiosError,
|
||||||
"deep-link",
|
"deep-link",
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -83,6 +83,11 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: "discover",
|
name: "discover",
|
||||||
component: () => import("../views/DiscoverView.vue"),
|
component: () => import("../views/DiscoverView.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/deep-link/:path*",
|
||||||
|
name: "deep-link",
|
||||||
|
component: () => import("../views/DeepLinkRedirectView.vue"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/gifted-details",
|
path: "/gifted-details",
|
||||||
name: "gifted-details",
|
name: "gifted-details",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import { logger } from "../utils/logger";
|
import { logger, safeStringify } from "../utils/logger";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles API errors with platform-specific logging and error processing.
|
* Handles API errors with platform-specific logging and error processing.
|
||||||
@@ -37,7 +37,8 @@ import { logger } from "../utils/logger";
|
|||||||
*/
|
*/
|
||||||
export const handleApiError = (error: AxiosError, endpoint: string) => {
|
export const handleApiError = (error: AxiosError, endpoint: string) => {
|
||||||
if (process.env.VITE_PLATFORM === "capacitor") {
|
if (process.env.VITE_PLATFORM === "capacitor") {
|
||||||
logger.error(`[Capacitor API Error] ${endpoint}:`, {
|
const endpointStr = safeStringify(endpoint); // we've seen this as an object in deep links
|
||||||
|
logger.error(`[Capacitor API Error] ${endpointStr}:`, {
|
||||||
message: error.message,
|
message: error.message,
|
||||||
status: error.response?.status,
|
status: error.response?.status,
|
||||||
data: error.response?.data,
|
data: error.response?.data,
|
||||||
|
|||||||
@@ -27,18 +27,16 @@
|
|||||||
* timesafari://<route>[/<param>][?queryParam1=value1&queryParam2=value2]
|
* timesafari://<route>[/<param>][?queryParam1=value1&queryParam2=value2]
|
||||||
*
|
*
|
||||||
* Supported Routes:
|
* Supported Routes:
|
||||||
* - user-profile: View user profile
|
|
||||||
* - project-details: View project details
|
|
||||||
* - onboard-meeting-setup: Setup onboarding meeting
|
|
||||||
* - invite-one-accept: Accept invitation
|
|
||||||
* - contact-import: Import contacts
|
|
||||||
* - confirm-gift: Confirm gift
|
|
||||||
* - claim: View claim
|
* - claim: View claim
|
||||||
* - claim-cert: View claim certificate
|
|
||||||
* - claim-add-raw: Add raw claim
|
* - claim-add-raw: Add raw claim
|
||||||
* - contact-edit: Edit contact
|
* - claim-cert: View claim certificate
|
||||||
* - contacts: View contacts
|
* - confirm-gift
|
||||||
|
* - contact-import: Import contacts
|
||||||
* - did: View DID
|
* - did: View DID
|
||||||
|
* - invite-one-accept: Accept invitation
|
||||||
|
* - onboard-meeting-members
|
||||||
|
* - project: View project details
|
||||||
|
* - user-profile: View user profile
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const handler = new DeepLinkHandler(router);
|
* const handler = new DeepLinkHandler(router);
|
||||||
@@ -81,18 +79,17 @@ export class DeepLinkHandler {
|
|||||||
string,
|
string,
|
||||||
{ name: string; paramKey?: string }
|
{ name: string; paramKey?: string }
|
||||||
> = {
|
> = {
|
||||||
"user-profile": { name: "user-profile" },
|
// note that similar lists are in src/interfaces/deepLinks.ts
|
||||||
"project-details": { name: "project-details" },
|
|
||||||
"onboard-meeting-setup": { name: "onboard-meeting-setup" },
|
|
||||||
"invite-one-accept": { name: "invite-one-accept" },
|
|
||||||
"contact-import": { name: "contact-import" },
|
|
||||||
"confirm-gift": { name: "confirm-gift" },
|
|
||||||
claim: { name: "claim" },
|
claim: { name: "claim" },
|
||||||
"claim-cert": { name: "claim-cert" },
|
|
||||||
"claim-add-raw": { name: "claim-add-raw" },
|
"claim-add-raw": { name: "claim-add-raw" },
|
||||||
"contact-edit": { name: "contact-edit", paramKey: "did" },
|
"claim-cert": { name: "claim-cert" },
|
||||||
contacts: { name: "contacts" },
|
"confirm-gift": { name: "confirm-gift" },
|
||||||
|
"contact-import": { name: "contact-import", paramKey: "jwt" },
|
||||||
did: { name: "did", paramKey: "did" },
|
did: { name: "did", paramKey: "did" },
|
||||||
|
"invite-one-accept": { name: "invite-one-accept", paramKey: "jwt" },
|
||||||
|
"onboard-meeting-members": { name: "onboard-meeting-members" },
|
||||||
|
project: { name: "project" },
|
||||||
|
"user-profile": { name: "user-profile" },
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -101,7 +98,7 @@ export class DeepLinkHandler {
|
|||||||
*
|
*
|
||||||
* @param url - The deep link URL to parse (format: scheme://path[?query])
|
* @param url - The deep link URL to parse (format: scheme://path[?query])
|
||||||
* @throws {DeepLinkError} If URL format is invalid
|
* @throws {DeepLinkError} If URL format is invalid
|
||||||
* @returns Parsed URL components (path, params, query)
|
* @returns Parsed URL components (path: string, params: {KEY: string}, query: {KEY: string})
|
||||||
*/
|
*/
|
||||||
private parseDeepLink(url: string) {
|
private parseDeepLink(url: string) {
|
||||||
const parts = url.split("://");
|
const parts = url.split("://");
|
||||||
@@ -117,7 +114,16 @@ export class DeepLinkHandler {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const [path, queryString] = parts[1].split("?");
|
const [path, queryString] = parts[1].split("?");
|
||||||
const [routePath, param] = path.split("/");
|
const [routePath, ...pathParams] = path.split("/");
|
||||||
|
// logger.info(
|
||||||
|
// "[DeepLink] Debug:",
|
||||||
|
// "Route Path:",
|
||||||
|
// routePath,
|
||||||
|
// "Path Params:",
|
||||||
|
// pathParams,
|
||||||
|
// "Query String:",
|
||||||
|
// queryString,
|
||||||
|
// );
|
||||||
|
|
||||||
// Validate route exists before proceeding
|
// Validate route exists before proceeding
|
||||||
if (!this.ROUTE_MAP[routePath]) {
|
if (!this.ROUTE_MAP[routePath]) {
|
||||||
@@ -136,45 +142,14 @@ export class DeepLinkHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const params: Record<string, string> = {};
|
const params: Record<string, string> = {};
|
||||||
if (param) {
|
if (pathParams) {
|
||||||
// Now we know routePath exists in ROUTE_MAP
|
// Now we know routePath exists in ROUTE_MAP
|
||||||
const routeConfig = this.ROUTE_MAP[routePath];
|
const routeConfig = this.ROUTE_MAP[routePath];
|
||||||
params[routeConfig.paramKey ?? "id"] = param;
|
params[routeConfig.paramKey ?? "id"] = pathParams.join("/");
|
||||||
}
|
}
|
||||||
return { path: routePath, params, query };
|
return { path: routePath, params, query };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Processes incoming deep links and routes them appropriately.
|
|
||||||
* Handles validation, error handling, and routing to the correct view.
|
|
||||||
*
|
|
||||||
* @param url - The deep link URL to process
|
|
||||||
* @throws {DeepLinkError} If URL processing fails
|
|
||||||
*/
|
|
||||||
async handleDeepLink(url: string): Promise<void> {
|
|
||||||
try {
|
|
||||||
logConsoleAndDb("[DeepLink] Processing URL: " + url, false);
|
|
||||||
const { path, params, query } = this.parseDeepLink(url);
|
|
||||||
// Ensure params is always a Record<string,string> by converting undefined to empty string
|
|
||||||
const sanitizedParams = Object.fromEntries(
|
|
||||||
Object.entries(params).map(([key, value]) => [key, value ?? ""]),
|
|
||||||
);
|
|
||||||
await this.validateAndRoute(path, sanitizedParams, query);
|
|
||||||
} catch (error) {
|
|
||||||
const deepLinkError = error as DeepLinkError;
|
|
||||||
logConsoleAndDb(
|
|
||||||
`[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.message}`,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
throw {
|
|
||||||
code: deepLinkError.code || "UNKNOWN_ERROR",
|
|
||||||
message: deepLinkError.message,
|
|
||||||
details: deepLinkError.details,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Routes the deep link to appropriate view with validated parameters.
|
* Routes the deep link to appropriate view with validated parameters.
|
||||||
* Validates route and parameters using Zod schemas before routing.
|
* Validates route and parameters using Zod schemas before routing.
|
||||||
@@ -245,6 +220,39 @@ export class DeepLinkHandler {
|
|||||||
code: "INVALID_PARAMETERS",
|
code: "INVALID_PARAMETERS",
|
||||||
message: (error as Error).message,
|
message: (error as Error).message,
|
||||||
details: error,
|
details: error,
|
||||||
|
params: params,
|
||||||
|
query: query,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes incoming deep links and routes them appropriately.
|
||||||
|
* Handles validation, error handling, and routing to the correct view.
|
||||||
|
*
|
||||||
|
* @param url - The deep link URL to process
|
||||||
|
* @throws {DeepLinkError} If URL processing fails
|
||||||
|
*/
|
||||||
|
async handleDeepLink(url: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
logConsoleAndDb("[DeepLink] Processing URL: " + url, false);
|
||||||
|
const { path, params, query } = this.parseDeepLink(url);
|
||||||
|
// Ensure params is always a Record<string,string> by converting undefined to empty string
|
||||||
|
const sanitizedParams = Object.fromEntries(
|
||||||
|
Object.entries(params).map(([key, value]) => [key, value ?? ""]),
|
||||||
|
);
|
||||||
|
await this.validateAndRoute(path, sanitizedParams, query);
|
||||||
|
} catch (error) {
|
||||||
|
const deepLinkError = error as DeepLinkError;
|
||||||
|
logConsoleAndDb(
|
||||||
|
`[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.message}`,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
throw {
|
||||||
|
code: deepLinkError.code || "UNKNOWN_ERROR",
|
||||||
|
message: deepLinkError.message,
|
||||||
|
details: deepLinkError.details,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
CameraSource,
|
CameraSource,
|
||||||
CameraDirection,
|
CameraDirection,
|
||||||
} from "@capacitor/camera";
|
} from "@capacitor/camera";
|
||||||
|
import { Capacitor } from "@capacitor/core";
|
||||||
import { Share } from "@capacitor/share";
|
import { Share } from "@capacitor/share";
|
||||||
import {
|
import {
|
||||||
SQLiteConnection,
|
SQLiteConnection,
|
||||||
@@ -247,7 +248,7 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
hasFileSystem: true,
|
hasFileSystem: true,
|
||||||
hasCamera: true,
|
hasCamera: true,
|
||||||
isMobile: true,
|
isMobile: true,
|
||||||
isIOS: /iPad|iPhone|iPod/.test(navigator.userAgent),
|
isIOS: Capacitor.getPlatform() === "ios",
|
||||||
hasFileDownload: false,
|
hasFileDownload: false,
|
||||||
needsFileHandlingInstructions: true,
|
needsFileHandlingInstructions: true,
|
||||||
isNativeApp: true,
|
isNativeApp: true,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { logToDb } from "../db/databaseUtil";
|
import { logToDb } from "../db/databaseUtil";
|
||||||
|
|
||||||
function safeStringify(obj: unknown) {
|
export function safeStringify(obj: unknown) {
|
||||||
const seen = new WeakSet();
|
const seen = new WeakSet();
|
||||||
|
|
||||||
return JSON.stringify(obj, (_key, value) => {
|
return JSON.stringify(obj, (_key, value) => {
|
||||||
@@ -67,8 +67,9 @@ export const logger = {
|
|||||||
// Errors will always be logged
|
// Errors will always be logged
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.error(message, ...args);
|
console.error(message, ...args);
|
||||||
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
|
const messageString = safeStringify(message);
|
||||||
logToDb(message + argsString);
|
const argsString = args.length > 0 ? safeStringify(args) : "";
|
||||||
|
logToDb(messageString + argsString);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -211,7 +211,7 @@
|
|||||||
@click="handleQRCodeClick"
|
@click="handleQRCodeClick"
|
||||||
>
|
>
|
||||||
Share Your Info
|
Share Your Info
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
@@ -1015,7 +1015,6 @@ import {
|
|||||||
retrieveSettingsForActiveAccount,
|
retrieveSettingsForActiveAccount,
|
||||||
updateAccountSettings,
|
updateAccountSettings,
|
||||||
} from "../db/index";
|
} from "../db/index";
|
||||||
import { Account } from "../db/tables/accounts";
|
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from "../db/tables/contacts";
|
||||||
import {
|
import {
|
||||||
DEFAULT_PASSKEY_EXPIRATION_MINUTES,
|
DEFAULT_PASSKEY_EXPIRATION_MINUTES,
|
||||||
@@ -1040,7 +1039,6 @@ import {
|
|||||||
} from "../libs/util";
|
} from "../libs/util";
|
||||||
import { UserProfile } from "@/libs/partnerServer";
|
import { UserProfile } from "@/libs/partnerServer";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
|
||||||
|
|
||||||
const inputImportFileNameRef = ref<Blob>();
|
const inputImportFileNameRef = ref<Blob>();
|
||||||
|
|
||||||
@@ -1174,8 +1172,6 @@ export default class AccountViewView extends Vue {
|
|||||||
5000,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
this.loadingProfile = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1198,6 +1194,8 @@ export default class AccountViewView extends Vue {
|
|||||||
},
|
},
|
||||||
5000,
|
5000,
|
||||||
);
|
);
|
||||||
|
} finally {
|
||||||
|
this.loadingProfile = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -1240,7 +1238,6 @@ export default class AccountViewView extends Vue {
|
|||||||
*/
|
*/
|
||||||
async initializeState() {
|
async initializeState() {
|
||||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||||
console.log("settings", settings);
|
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
await db.open();
|
await db.open();
|
||||||
settings = await retrieveSettingsForActiveAccount();
|
settings = await retrieveSettingsForActiveAccount();
|
||||||
@@ -1817,7 +1814,7 @@ export default class AccountViewView extends Vue {
|
|||||||
if (!this.isRegistered) {
|
if (!this.isRegistered) {
|
||||||
// the user was not known to be registered, but now they are (because we got no error) so let's record it
|
// the user was not known to be registered, but now they are (because we got no error) so let's record it
|
||||||
try {
|
try {
|
||||||
await databaseUtil.updateAccountSettings(did, {
|
await databaseUtil.updateDidSpecificSettings(did, {
|
||||||
isRegistered: true,
|
isRegistered: true,
|
||||||
});
|
});
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
@@ -2021,7 +2018,7 @@ export default class AccountViewView extends Vue {
|
|||||||
if ((error as any).response.status === 404) {
|
if ((error as any).response.status === 404) {
|
||||||
logger.error("The image was already deleted:", error);
|
logger.error("The image was already deleted:", error);
|
||||||
|
|
||||||
await databaseUtil.updateAccountSettings(this.activeDid, {
|
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
|
||||||
profileImageUrl: undefined,
|
profileImageUrl: undefined,
|
||||||
});
|
});
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ export default class ClaimAddRawView extends Vue {
|
|||||||
this.apiServer,
|
this.apiServer,
|
||||||
this.axios,
|
this.axios,
|
||||||
);
|
);
|
||||||
if (result.type === "success") {
|
if (result.success) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
|
|||||||
@@ -46,23 +46,35 @@
|
|||||||
</h2>
|
</h2>
|
||||||
<div class="flex justify-center w-full">
|
<div class="flex justify-center w-full">
|
||||||
<router-link
|
<router-link
|
||||||
|
v-if="veriClaim.id"
|
||||||
:to="'/claim-cert/' + encodeURIComponent(veriClaim.id)"
|
:to="'/claim-cert/' + encodeURIComponent(veriClaim.id)"
|
||||||
class="text-blue-500 mt-2"
|
class="text-blue-500 mt-2"
|
||||||
title="Printable Certificate"
|
title="View Printable Certificate"
|
||||||
>
|
>
|
||||||
<font-awesome
|
<font-awesome
|
||||||
icon="square"
|
icon="square"
|
||||||
class="text-white bg-yellow-500 p-1"
|
class="text-white bg-yellow-500 p-1"
|
||||||
/>
|
/>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<button
|
||||||
|
v-if="veriClaim.id"
|
||||||
|
class="text-blue-500 ml-2 mt-2"
|
||||||
|
title="Copy Printable Certificate Link"
|
||||||
|
@click="
|
||||||
|
copyToClipboard(
|
||||||
|
'A link to the certificate page',
|
||||||
|
`${APP_SERVER}/deep-link/claim-cert/${veriClaim.id}`,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<font-awesome icon="link" class="text-yellow-500 p-1" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<!-- show link icon to copy this URL to the clipboard -->
|
<!-- show link icon to copy this URL to the clipboard -->
|
||||||
<div class="flex justify-end w-full">
|
<div class="flex justify-end w-full">
|
||||||
<button
|
<button
|
||||||
title="Copy Link"
|
title="Copy Link"
|
||||||
@click="
|
@click="copyToClipboard('A link to this page', windowDeepLink)"
|
||||||
copyToClipboard('A link to this page', window.location.href)
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<font-awesome icon="link" class="text-slate-500" />
|
<font-awesome icon="link" class="text-slate-500" />
|
||||||
</button>
|
</button>
|
||||||
@@ -292,15 +304,17 @@
|
|||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
{{ didInfo(confirmerId) }}
|
{{ didInfo(confirmerId) }}
|
||||||
<span v-if="!serverUtil.isEmptyOrHiddenDid(confirmerId)">
|
<span v-if="!serverUtil.isEmptyOrHiddenDid(confirmerId)">
|
||||||
<a
|
<router-link
|
||||||
:href="`/did/${confirmerId}`"
|
:to="{
|
||||||
|
path: '/did/' + encodeURIComponent(confirmerId),
|
||||||
|
}"
|
||||||
class="text-blue-500"
|
class="text-blue-500"
|
||||||
>
|
>
|
||||||
<font-awesome
|
<font-awesome
|
||||||
icon="arrow-up-right-from-square"
|
icon="arrow-up-right-from-square"
|
||||||
class="fa-fw"
|
class="fa-fw"
|
||||||
/>
|
/>
|
||||||
</a>
|
</router-link>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -332,15 +346,17 @@
|
|||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
{{ didInfo(confsVisibleTo) }}
|
{{ didInfo(confsVisibleTo) }}
|
||||||
<span v-if="!serverUtil.isEmptyOrHiddenDid(confsVisibleTo)">
|
<span v-if="!serverUtil.isEmptyOrHiddenDid(confsVisibleTo)">
|
||||||
<a
|
<router-link
|
||||||
:href="`/did/${confsVisibleTo}`"
|
:to="{
|
||||||
|
path: '/did/' + encodeURIComponent(confsVisibleTo),
|
||||||
|
}"
|
||||||
class="text-blue-500"
|
class="text-blue-500"
|
||||||
>
|
>
|
||||||
<font-awesome
|
<font-awesome
|
||||||
icon="arrow-up-right-from-square"
|
icon="arrow-up-right-from-square"
|
||||||
class="fa-fw"
|
class="fa-fw"
|
||||||
/>
|
/>
|
||||||
</a>
|
</router-link>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -400,7 +416,7 @@
|
|||||||
contacts can see more details:
|
contacts can see more details:
|
||||||
<a
|
<a
|
||||||
class="text-blue-500"
|
class="text-blue-500"
|
||||||
@click="copyToClipboard('A link to this page', windowLocation)"
|
@click="copyToClipboard('A link to this page', windowDeepLink)"
|
||||||
>click to copy this page info</a
|
>click to copy this page info</a
|
||||||
>
|
>
|
||||||
and see if they can make an introduction. Someone is connected to
|
and see if they can make an introduction. Someone is connected to
|
||||||
@@ -423,7 +439,7 @@
|
|||||||
If you'd like an introduction,
|
If you'd like an introduction,
|
||||||
<a
|
<a
|
||||||
class="text-blue-500"
|
class="text-blue-500"
|
||||||
@click="copyToClipboard('A link to this page', windowLocation)"
|
@click="copyToClipboard('A link to this page', windowDeepLink)"
|
||||||
>share this page with them and ask if they'll tell you more about
|
>share this page with them and ask if they'll tell you more about
|
||||||
about the participants.</a
|
about the participants.</a
|
||||||
>
|
>
|
||||||
@@ -449,15 +465,17 @@
|
|||||||
<span>
|
<span>
|
||||||
{{ didInfo(visDid) }}
|
{{ didInfo(visDid) }}
|
||||||
<span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)">
|
<span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)">
|
||||||
<a
|
<router-link
|
||||||
:href="`/did/${visDid}`"
|
:to="{
|
||||||
|
path: '/did/' + encodeURIComponent(visDid),
|
||||||
|
}"
|
||||||
class="text-blue-500"
|
class="text-blue-500"
|
||||||
>
|
>
|
||||||
<font-awesome
|
<font-awesome
|
||||||
icon="arrow-up-right-from-square"
|
icon="arrow-up-right-from-square"
|
||||||
class="fa-fw"
|
class="fa-fw"
|
||||||
/>
|
/>
|
||||||
</a>
|
</router-link>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="veriClaim.publicUrls?.[visDid]"
|
<span v-if="veriClaim.publicUrls?.[visDid]"
|
||||||
>, found at <a
|
>, found at <a
|
||||||
@@ -539,7 +557,7 @@ import { useClipboard } from "@vueuse/core";
|
|||||||
import { GenericVerifiableCredential } from "../interfaces";
|
import { GenericVerifiableCredential } from "../interfaces";
|
||||||
import GiftedDialog from "../components/GiftedDialog.vue";
|
import GiftedDialog from "../components/GiftedDialog.vue";
|
||||||
import QuickNav from "../components/QuickNav.vue";
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
import { APP_SERVER, NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||||
import * as databaseUtil from "../db/databaseUtil";
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
import { db } from "../db/index";
|
import { db } from "../db/index";
|
||||||
import { logConsoleAndDb } from "../db/databaseUtil";
|
import { logConsoleAndDb } from "../db/databaseUtil";
|
||||||
@@ -586,8 +604,9 @@ export default class ClaimView extends Vue {
|
|||||||
veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
|
veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
|
||||||
veriClaimDump = "";
|
veriClaimDump = "";
|
||||||
veriClaimDidsVisible: { [key: string]: string[] } = {};
|
veriClaimDidsVisible: { [key: string]: string[] } = {};
|
||||||
windowLocation = window.location.href;
|
windowDeepLink = window.location.href; // changed in the setup for deep linking
|
||||||
|
|
||||||
|
APP_SERVER = APP_SERVER;
|
||||||
R = R;
|
R = R;
|
||||||
yaml = yaml;
|
yaml = yaml;
|
||||||
libsUtil = libsUtil;
|
libsUtil = libsUtil;
|
||||||
@@ -664,6 +683,7 @@ export default class ClaimView extends Vue {
|
|||||||
5000,
|
5000,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
this.windowDeepLink = `${APP_SERVER}/deep-link/claim/${claimId}`;
|
||||||
|
|
||||||
this.canShare = !!navigator.share;
|
this.canShare = !!navigator.share;
|
||||||
}
|
}
|
||||||
@@ -934,7 +954,7 @@ export default class ClaimView extends Vue {
|
|||||||
this.apiServer,
|
this.apiServer,
|
||||||
this.axios,
|
this.axios,
|
||||||
);
|
);
|
||||||
if (result.type === "success") {
|
if (result.success) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -999,11 +1019,11 @@ export default class ClaimView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onClickShareClaim() {
|
onClickShareClaim() {
|
||||||
this.copyToClipboard("A link to this page", this.windowLocation);
|
this.copyToClipboard("A link to this page", this.windowDeepLink);
|
||||||
window.navigator.share({
|
window.navigator.share({
|
||||||
title: "Help Connect Me",
|
title: "Help Connect Me",
|
||||||
text: "I'm trying to find the people who recorded this. Can you help me?",
|
text: "I'm trying to find the people who recorded this. Can you help me?",
|
||||||
url: this.windowLocation,
|
url: this.windowDeepLink,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -407,14 +407,14 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 ml-2">
|
<div class="mt-2 ml-2">
|
||||||
<a
|
<router-link
|
||||||
v-if="isRegistered"
|
v-if="isRegistered"
|
||||||
class="text-blue-500 cursor-pointer"
|
class="text-blue-500 cursor-pointer"
|
||||||
:href="urlForNewGive"
|
:to="urlForNewGive"
|
||||||
>
|
>
|
||||||
<font-awesome icon="file-lines" />
|
<font-awesome icon="file-lines" />
|
||||||
Record a Give Similar to the Original
|
Record a Give Similar to the Original
|
||||||
</a>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -436,7 +436,7 @@ import { Component, Vue } from "vue-facing-decorator";
|
|||||||
import { useClipboard } from "@vueuse/core";
|
import { useClipboard } from "@vueuse/core";
|
||||||
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||||
import QuickNav from "../components/QuickNav.vue";
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
import { APP_SERVER, NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from "../db/tables/contacts";
|
||||||
import * as databaseUtil from "../db/databaseUtil";
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
@@ -494,7 +494,7 @@ export default class ConfirmGiftView extends Vue {
|
|||||||
veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
|
veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
|
||||||
veriClaimDump = "";
|
veriClaimDump = "";
|
||||||
veriClaimDidsVisible: { [key: string]: string[] } = {};
|
veriClaimDidsVisible: { [key: string]: string[] } = {};
|
||||||
windowLocation = window.location.href;
|
windowLocation = window.location.href; // this is changed to a deep link in the setup
|
||||||
|
|
||||||
R = R;
|
R = R;
|
||||||
yaml = yaml;
|
yaml = yaml;
|
||||||
@@ -566,6 +566,9 @@ export default class ConfirmGiftView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const claimId = decodeURIComponent(pathParam);
|
const claimId = decodeURIComponent(pathParam);
|
||||||
|
|
||||||
|
this.windowLocation = APP_SERVER + "/deep-link/confirm-gift/" + claimId;
|
||||||
|
|
||||||
await this.loadClaim(claimId, this.activeDid);
|
await this.loadClaim(claimId, this.activeDid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -676,12 +679,12 @@ export default class ConfirmGiftView extends Vue {
|
|||||||
/**
|
/**
|
||||||
* Add participant (giver/recipient) name & URL info
|
* Add participant (giver/recipient) name & URL info
|
||||||
*/
|
*/
|
||||||
|
this.giverName = this.didInfo(this.giveDetails?.agentDid);
|
||||||
if (this.giveDetails?.agentDid) {
|
if (this.giveDetails?.agentDid) {
|
||||||
this.giverName = this.didInfo(this.giveDetails.agentDid);
|
|
||||||
this.urlForNewGive += `&giverDid=${encodeURIComponent(this.giveDetails.agentDid)}&giverName=${encodeURIComponent(this.giverName)}`;
|
this.urlForNewGive += `&giverDid=${encodeURIComponent(this.giveDetails.agentDid)}&giverName=${encodeURIComponent(this.giverName)}`;
|
||||||
}
|
}
|
||||||
|
this.recipientName = this.didInfo(this.giveDetails?.recipientDid);
|
||||||
if (this.giveDetails?.recipientDid) {
|
if (this.giveDetails?.recipientDid) {
|
||||||
this.recipientName = this.didInfo(this.giveDetails.recipientDid);
|
|
||||||
this.urlForNewGive += `&recipientDid=${encodeURIComponent(this.giveDetails.recipientDid)}&recipientName=${encodeURIComponent(this.recipientName)}`;
|
this.urlForNewGive += `&recipientDid=${encodeURIComponent(this.giveDetails.recipientDid)}&recipientName=${encodeURIComponent(this.recipientName)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -831,7 +834,7 @@ export default class ConfirmGiftView extends Vue {
|
|||||||
this.apiServer,
|
this.apiServer,
|
||||||
this.axios,
|
this.axios,
|
||||||
);
|
);
|
||||||
if (result.type === "success") {
|
if (result.success) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
|
|||||||
@@ -138,11 +138,13 @@ import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
|||||||
|
|
||||||
import QuickNav from "../components/QuickNav.vue";
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
import TopMessage from "../components/TopMessage.vue";
|
import TopMessage from "../components/TopMessage.vue";
|
||||||
import { AppString, NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||||
import { db } from "../db/index";
|
|
||||||
import { Contact, ContactMethod } from "../db/tables/contacts";
|
|
||||||
import * as databaseUtil from "../db/databaseUtil";
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
import { parseJsonField } from "../db/databaseUtil";
|
||||||
|
import { db } from "../db/index";
|
||||||
|
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
||||||
|
import { Contact, ContactMethod } from "../db/tables/contacts";
|
||||||
|
import { AppString } from "../constants/app";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contact Edit View Component
|
* Contact Edit View Component
|
||||||
@@ -230,9 +232,7 @@ export default class ContactEditView extends Vue {
|
|||||||
let contact: Contact | undefined = databaseUtil.mapQueryResultToValues(
|
let contact: Contact | undefined = databaseUtil.mapQueryResultToValues(
|
||||||
dbContact,
|
dbContact,
|
||||||
)[0] as unknown as Contact;
|
)[0] as unknown as Contact;
|
||||||
contact.contactMethods = JSON.parse(
|
contact.contactMethods = parseJsonField(contact?.contactMethods, []);
|
||||||
(contact?.contactMethods as unknown as string) || "[]",
|
|
||||||
);
|
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
await db.open();
|
await db.open();
|
||||||
contact = await db.contacts.get(contactDid || "");
|
contact = await db.contacts.get(contactDid || "");
|
||||||
|
|||||||
@@ -213,6 +213,7 @@ import {
|
|||||||
} from "../db/index";
|
} from "../db/index";
|
||||||
import { Contact, ContactMethod } from "../db/tables/contacts";
|
import { Contact, ContactMethod } from "../db/tables/contacts";
|
||||||
import * as databaseUtil from "../db/databaseUtil";
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
|
import { parseJsonField } from "../db/databaseUtil";
|
||||||
import * as libsUtil from "../libs/util";
|
import * as libsUtil from "../libs/util";
|
||||||
import {
|
import {
|
||||||
capitalizeAndInsertSpacesBeforeCaps,
|
capitalizeAndInsertSpacesBeforeCaps,
|
||||||
@@ -289,7 +290,7 @@ function dbRecordToContact(record: ContactDbRecord): Contact {
|
|||||||
profileImageUrl: safeString(record.profileImageUrl),
|
profileImageUrl: safeString(record.profileImageUrl),
|
||||||
publicKeyBase64: safeString(record.publicKeyBase64),
|
publicKeyBase64: safeString(record.publicKeyBase64),
|
||||||
nextPubKeyHashB64: safeString(record.nextPubKeyHashB64),
|
nextPubKeyHashB64: safeString(record.nextPubKeyHashB64),
|
||||||
contactMethods: JSON.parse(record.contactMethods || "[]"),
|
contactMethods: parseJsonField(record.contactMethods, []),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -104,6 +104,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { Buffer } from "buffer/";
|
||||||
import QRCodeVue3 from "qr-code-generator-vue3";
|
import QRCodeVue3 from "qr-code-generator-vue3";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
@@ -117,13 +118,20 @@ import { db } from "../db/index";
|
|||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from "../db/tables/contacts";
|
||||||
import { getContactJwtFromJwtUrl } from "../libs/crypto";
|
import { getContactJwtFromJwtUrl } from "../libs/crypto";
|
||||||
import { decodeEndorserJwt, ETHR_DID_PREFIX } from "../libs/crypto/vc";
|
import { decodeEndorserJwt, ETHR_DID_PREFIX } from "../libs/crypto/vc";
|
||||||
|
import * as libsUtil from "../libs/util";
|
||||||
import { retrieveSettingsForActiveAccount } from "../db/index";
|
import { retrieveSettingsForActiveAccount } from "../db/index";
|
||||||
import * as databaseUtil from "../db/databaseUtil";
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
import { setVisibilityUtil } from "../libs/endorserServer";
|
import {
|
||||||
|
CONTACT_CSV_HEADER,
|
||||||
|
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
|
||||||
|
generateEndorserJwtUrlForAccount,
|
||||||
|
setVisibilityUtil,
|
||||||
|
} from "../libs/endorserServer";
|
||||||
import UserNameDialog from "../components/UserNameDialog.vue";
|
import UserNameDialog from "../components/UserNameDialog.vue";
|
||||||
import { generateEndorserJwtUrlForAccount } from "../libs/endorserServer";
|
|
||||||
import { retrieveAccountMetadata } from "../libs/util";
|
import { retrieveAccountMetadata } from "../libs/util";
|
||||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||||
|
import { parseJsonField } from "../db/databaseUtil";
|
||||||
|
import { Account } from "@/db/tables/accounts";
|
||||||
|
|
||||||
interface QRScanResult {
|
interface QRScanResult {
|
||||||
rawValue?: string;
|
rawValue?: string;
|
||||||
@@ -141,7 +149,7 @@ interface IUserNameDialog {
|
|||||||
UserNameDialog,
|
UserNameDialog,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class ContactQRScan extends Vue {
|
export default class ContactQRScanFull extends Vue {
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||||
$router!: Router;
|
$router!: Router;
|
||||||
|
|
||||||
@@ -150,6 +158,8 @@ export default class ContactQRScan extends Vue {
|
|||||||
activeDid = "";
|
activeDid = "";
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
givenName = "";
|
givenName = "";
|
||||||
|
isRegistered = false;
|
||||||
|
profileImageUrl = "";
|
||||||
qrValue = "";
|
qrValue = "";
|
||||||
ETHR_DID_PREFIX = ETHR_DID_PREFIX;
|
ETHR_DID_PREFIX = ETHR_DID_PREFIX;
|
||||||
|
|
||||||
@@ -171,19 +181,22 @@ export default class ContactQRScan extends Vue {
|
|||||||
this.activeDid = settings.activeDid || "";
|
this.activeDid = settings.activeDid || "";
|
||||||
this.apiServer = settings.apiServer || "";
|
this.apiServer = settings.apiServer || "";
|
||||||
this.givenName = settings.firstName || "";
|
this.givenName = settings.firstName || "";
|
||||||
|
this.isRegistered = !!settings.isRegistered;
|
||||||
|
this.profileImageUrl = settings.profileImageUrl || "";
|
||||||
|
|
||||||
const account = await retrieveAccountMetadata(this.activeDid);
|
const account = await retrieveAccountMetadata(this.activeDid);
|
||||||
if (account) {
|
if (account) {
|
||||||
const name =
|
const name =
|
||||||
(settings.firstName || "") +
|
(settings.firstName || "") +
|
||||||
(settings.lastName ? ` ${settings.lastName}` : "");
|
(settings.lastName ? ` ${settings.lastName}` : "");
|
||||||
this.qrValue = await generateEndorserJwtUrlForAccount(
|
const publicKeyBase64 = Buffer.from(
|
||||||
account,
|
account.publicKeyHex,
|
||||||
!!settings.isRegistered,
|
"hex",
|
||||||
name,
|
).toString("base64");
|
||||||
settings.profileImageUrl || "",
|
this.qrValue =
|
||||||
false,
|
CONTACT_CSV_HEADER +
|
||||||
);
|
"\n" +
|
||||||
|
`"${name}",${account.did},${publicKeyBase64},false,${this.isRegistered}`;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error initializing component:", {
|
logger.error("Error initializing component:", {
|
||||||
@@ -335,57 +348,69 @@ export default class ContactQRScan extends Vue {
|
|||||||
|
|
||||||
logger.info("Processing QR code scan result:", rawValue);
|
logger.info("Processing QR code scan result:", rawValue);
|
||||||
|
|
||||||
// Extract JWT
|
let contact: Contact;
|
||||||
const jwt = getContactJwtFromJwtUrl(rawValue);
|
if (rawValue.includes(CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI)) {
|
||||||
if (!jwt) {
|
// Extract JWT
|
||||||
logger.warn("Invalid QR code format - no JWT found in URL");
|
const jwt = getContactJwtFromJwtUrl(rawValue);
|
||||||
|
if (!jwt) {
|
||||||
|
logger.warn("Invalid QR code format - no JWT found in URL");
|
||||||
|
this.$notify({
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Invalid QR Code",
|
||||||
|
text: "This QR code does not contain valid contact information. Scan a TimeSafari contact QR code.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process JWT and contact info
|
||||||
|
logger.info("Decoding JWT payload from QR code");
|
||||||
|
const decodedJwt = await decodeEndorserJwt(jwt);
|
||||||
|
if (!decodedJwt?.payload?.own) {
|
||||||
|
logger.warn("Invalid JWT payload - missing 'own' field");
|
||||||
|
this.$notify({
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Invalid Contact Info",
|
||||||
|
text: "The contact information is incomplete or invalid.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contactInfo = decodedJwt.payload.own;
|
||||||
|
const did = contactInfo.did || decodedJwt.payload.iss;
|
||||||
|
if (!did) {
|
||||||
|
logger.warn("Invalid contact info - missing DID");
|
||||||
|
this.$notify({
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Invalid Contact",
|
||||||
|
text: "The contact DID is missing.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create contact object
|
||||||
|
contact = {
|
||||||
|
did: did,
|
||||||
|
name: contactInfo.name || "",
|
||||||
|
publicKeyBase64: contactInfo.publicKeyBase64 || "",
|
||||||
|
seesMe: contactInfo.seesMe || false,
|
||||||
|
registered: contactInfo.registered || false,
|
||||||
|
};
|
||||||
|
} else if (rawValue.startsWith(CONTACT_CSV_HEADER)) {
|
||||||
|
const lines = rawValue.split(/\n/);
|
||||||
|
contact = libsUtil.csvLineToContact(lines[1]);
|
||||||
|
} else {
|
||||||
this.$notify({
|
this.$notify({
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Invalid QR Code",
|
title: "Error",
|
||||||
text: "This QR code does not contain valid contact information. Please scan a TimeSafari contact QR code.",
|
text: "Could not determine the type of contact info. Try again, or tap the QR code to copy it and send it to them.",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process JWT and contact info
|
|
||||||
logger.info("Decoding JWT payload from QR code");
|
|
||||||
const decodedJwt = await decodeEndorserJwt(jwt);
|
|
||||||
if (!decodedJwt?.payload?.own) {
|
|
||||||
logger.warn("Invalid JWT payload - missing 'own' field");
|
|
||||||
this.$notify({
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Invalid Contact Info",
|
|
||||||
text: "The contact information is incomplete or invalid.",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const contactInfo = decodedJwt.payload.own;
|
|
||||||
const did = contactInfo.did || decodedJwt.payload.iss;
|
|
||||||
if (!did) {
|
|
||||||
logger.warn("Invalid contact info - missing DID");
|
|
||||||
this.$notify({
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Invalid Contact",
|
|
||||||
text: "The contact DID is missing.",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create contact object
|
|
||||||
const contact = {
|
|
||||||
did: did,
|
|
||||||
name: contactInfo.name || "",
|
|
||||||
email: contactInfo.email || "",
|
|
||||||
phone: contactInfo.phone || "",
|
|
||||||
company: contactInfo.company || "",
|
|
||||||
title: contactInfo.title || "",
|
|
||||||
notes: contactInfo.notes || "",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add contact but keep scanning
|
// Add contact but keep scanning
|
||||||
logger.info("Adding new contact to database:", {
|
logger.info("Adding new contact to database:", {
|
||||||
did: contact.did,
|
did: contact.did,
|
||||||
@@ -467,14 +492,16 @@ export default class ContactQRScan extends Vue {
|
|||||||
title: "Contact Exists",
|
title: "Contact Exists",
|
||||||
text: "This contact has already been added to your list.",
|
text: "This contact has already been added to your list.",
|
||||||
},
|
},
|
||||||
3000,
|
5000,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add new contact
|
// Add new contact
|
||||||
// @ts-expect-error because we're just using the value to store to the DB
|
// @ts-expect-error because we're just using the value to store to the DB
|
||||||
contact.contactMethods = JSON.stringify(contact.contactMethods);
|
contact.contactMethods = JSON.stringify(
|
||||||
|
parseJsonField(contact.contactMethods, []),
|
||||||
|
);
|
||||||
const { sql, params } = databaseUtil.generateInsertStatement(
|
const { sql, params } = databaseUtil.generateInsertStatement(
|
||||||
contact as unknown as Record<string, unknown>,
|
contact as unknown as Record<string, unknown>,
|
||||||
"contacts",
|
"contacts",
|
||||||
@@ -565,9 +592,19 @@ export default class ContactQRScan extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onCopyUrlToClipboard() {
|
async onCopyUrlToClipboard() {
|
||||||
|
const account = (await libsUtil.retrieveFullyDecryptedAccount(
|
||||||
|
this.activeDid,
|
||||||
|
)) as Account;
|
||||||
|
const jwtUrl = await generateEndorserJwtUrlForAccount(
|
||||||
|
account,
|
||||||
|
this.isRegistered,
|
||||||
|
this.givenName,
|
||||||
|
this.profileImageUrl,
|
||||||
|
true,
|
||||||
|
);
|
||||||
useClipboard()
|
useClipboard()
|
||||||
.copy(this.qrValue)
|
.copy(jwtUrl)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -159,6 +159,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
|
import { Buffer } from "buffer/";
|
||||||
import QRCodeVue3 from "qr-code-generator-vue3";
|
import QRCodeVue3 from "qr-code-generator-vue3";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { useClipboard } from "@vueuse/core";
|
import { useClipboard } from "@vueuse/core";
|
||||||
@@ -171,19 +172,23 @@ import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
|||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from "../db/tables/contacts";
|
||||||
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
||||||
import * as databaseUtil from "../db/databaseUtil";
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
|
import { parseJsonField } from "../db/databaseUtil";
|
||||||
import { getContactJwtFromJwtUrl } from "../libs/crypto";
|
import { getContactJwtFromJwtUrl } from "../libs/crypto";
|
||||||
import {
|
import {
|
||||||
|
CONTACT_CSV_HEADER,
|
||||||
|
CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI,
|
||||||
generateEndorserJwtUrlForAccount,
|
generateEndorserJwtUrlForAccount,
|
||||||
register,
|
register,
|
||||||
setVisibilityUtil,
|
setVisibilityUtil,
|
||||||
} from "../libs/endorserServer";
|
} from "../libs/endorserServer";
|
||||||
import { decodeEndorserJwt, ETHR_DID_PREFIX } from "../libs/crypto/vc";
|
import { decodeEndorserJwt, ETHR_DID_PREFIX } from "../libs/crypto/vc";
|
||||||
import { retrieveAccountMetadata } from "../libs/util";
|
import * as libsUtil from "../libs/util";
|
||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { QRScannerFactory } from "@/services/QRScanner/QRScannerFactory";
|
import { QRScannerFactory } from "@/services/QRScanner/QRScannerFactory";
|
||||||
import { CameraState } from "@/services/QRScanner/types";
|
import { CameraState } from "@/services/QRScanner/types";
|
||||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||||
|
import { Account } from "@/db/tables/accounts";
|
||||||
|
|
||||||
interface QRScanResult {
|
interface QRScanResult {
|
||||||
rawValue?: string;
|
rawValue?: string;
|
||||||
@@ -213,6 +218,7 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
isRegistered = false;
|
isRegistered = false;
|
||||||
qrValue = "";
|
qrValue = "";
|
||||||
isScanning = false;
|
isScanning = false;
|
||||||
|
profileImageUrl = "";
|
||||||
error: string | null = null;
|
error: string | null = null;
|
||||||
|
|
||||||
// QR Scanner properties
|
// QR Scanner properties
|
||||||
@@ -250,19 +256,21 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
this.hideRegisterPromptOnNewContact =
|
this.hideRegisterPromptOnNewContact =
|
||||||
!!settings.hideRegisterPromptOnNewContact;
|
!!settings.hideRegisterPromptOnNewContact;
|
||||||
this.isRegistered = !!settings.isRegistered;
|
this.isRegistered = !!settings.isRegistered;
|
||||||
|
this.profileImageUrl = settings.profileImageUrl || "";
|
||||||
|
|
||||||
const account = await retrieveAccountMetadata(this.activeDid);
|
const account = await libsUtil.retrieveAccountMetadata(this.activeDid);
|
||||||
if (account) {
|
if (account) {
|
||||||
const name =
|
const name =
|
||||||
(settings.firstName || "") +
|
(settings.firstName || "") +
|
||||||
(settings.lastName ? ` ${settings.lastName}` : "");
|
(settings.lastName ? ` ${settings.lastName}` : "");
|
||||||
this.qrValue = await generateEndorserJwtUrlForAccount(
|
const publicKeyBase64 = Buffer.from(
|
||||||
account,
|
account.publicKeyHex,
|
||||||
!!settings.isRegistered,
|
"hex",
|
||||||
name,
|
).toString("base64");
|
||||||
settings.profileImageUrl || "",
|
this.qrValue =
|
||||||
false,
|
CONTACT_CSV_HEADER +
|
||||||
);
|
"\n" +
|
||||||
|
`"${name}",${account.did},${publicKeyBase64},false,${this.isRegistered}`;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error initializing component:", {
|
logger.error("Error initializing component:", {
|
||||||
@@ -273,7 +281,7 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Initialization Error",
|
title: "Initialization Error",
|
||||||
text: "Failed to initialize QR scanner. Please try again.",
|
text: "Failed to initialize QR renderer or scanner. Please try again.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -460,53 +468,68 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
|
|
||||||
logger.info("Processing QR code scan result:", rawValue);
|
logger.info("Processing QR code scan result:", rawValue);
|
||||||
|
|
||||||
// Extract JWT
|
let contact: Contact;
|
||||||
const jwt = getContactJwtFromJwtUrl(rawValue);
|
if (rawValue.includes(CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI)) {
|
||||||
if (!jwt) {
|
const jwt = getContactJwtFromJwtUrl(rawValue);
|
||||||
logger.warn("Invalid QR code format - no JWT found in URL");
|
if (!jwt) {
|
||||||
|
logger.warn("Invalid QR code format - no JWT found in URL");
|
||||||
|
this.$notify({
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Invalid QR Code",
|
||||||
|
text: "This QR code does not contain valid contact information. Scan a TimeSafari contact QR code.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.info("Decoding JWT payload from QR code");
|
||||||
|
const decodedJwt = await decodeEndorserJwt(jwt);
|
||||||
|
|
||||||
|
// Process JWT and contact info
|
||||||
|
if (!decodedJwt?.payload?.own) {
|
||||||
|
logger.warn("Invalid JWT payload - missing 'own' field");
|
||||||
|
this.$notify({
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Invalid Contact Info",
|
||||||
|
text: "The contact information is incomplete or invalid.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contactInfo = decodedJwt.payload.own;
|
||||||
|
const did = contactInfo.did || decodedJwt.payload.iss;
|
||||||
|
if (!did) {
|
||||||
|
logger.warn("Invalid contact info - missing DID");
|
||||||
|
this.$notify({
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Invalid Contact",
|
||||||
|
text: "The contact DID is missing.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create contact object
|
||||||
|
contact = {
|
||||||
|
did: did,
|
||||||
|
name: contactInfo.name || "",
|
||||||
|
publicKeyBase64: contactInfo.publicKeyBase64 || "",
|
||||||
|
seesMe: contactInfo.seesMe || false,
|
||||||
|
registered: contactInfo.registered || false,
|
||||||
|
};
|
||||||
|
} else if (rawValue.startsWith(CONTACT_CSV_HEADER)) {
|
||||||
|
const lines = rawValue.split(/\n/);
|
||||||
|
contact = libsUtil.csvLineToContact(lines[1]);
|
||||||
|
} else {
|
||||||
this.$notify({
|
this.$notify({
|
||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Invalid QR Code",
|
title: "Error",
|
||||||
text: "This QR code does not contain valid contact information. Please scan a TimeSafari contact QR code.",
|
text: "Could not determine the type of contact info. Try again, or tap the QR code to copy it and send it to them.",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process JWT and contact info
|
|
||||||
logger.info("Decoding JWT payload from QR code");
|
|
||||||
const decodedJwt = await decodeEndorserJwt(jwt);
|
|
||||||
if (!decodedJwt?.payload?.own) {
|
|
||||||
logger.warn("Invalid JWT payload - missing 'own' field");
|
|
||||||
this.$notify({
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Invalid Contact Info",
|
|
||||||
text: "The contact information is incomplete or invalid.",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const contactInfo = decodedJwt.payload.own;
|
|
||||||
const did = contactInfo.did || decodedJwt.payload.iss;
|
|
||||||
if (!did) {
|
|
||||||
logger.warn("Invalid contact info - missing DID");
|
|
||||||
this.$notify({
|
|
||||||
group: "alert",
|
|
||||||
type: "danger",
|
|
||||||
title: "Invalid Contact",
|
|
||||||
text: "The contact DID is missing.",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create contact object
|
|
||||||
const contact = {
|
|
||||||
did: did,
|
|
||||||
name: contactInfo.name || "",
|
|
||||||
notes: contactInfo.notes || "",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add contact but keep scanning
|
// Add contact but keep scanning
|
||||||
logger.info("Adding new contact to database:", {
|
logger.info("Adding new contact to database:", {
|
||||||
did: contact.did,
|
did: contact.did,
|
||||||
@@ -648,12 +671,20 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onCopyUrlToClipboard() {
|
async onCopyUrlToClipboard() {
|
||||||
//this.onScanDetect([{ rawValue: this.qrValue }]); // good for testing
|
const account = (await libsUtil.retrieveFullyDecryptedAccount(
|
||||||
|
this.activeDid,
|
||||||
|
)) as Account;
|
||||||
|
const jwtUrl = await generateEndorserJwtUrlForAccount(
|
||||||
|
account,
|
||||||
|
this.isRegistered,
|
||||||
|
this.givenName,
|
||||||
|
this.profileImageUrl,
|
||||||
|
true,
|
||||||
|
);
|
||||||
useClipboard()
|
useClipboard()
|
||||||
.copy(this.qrValue)
|
.copy(jwtUrl)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// console.log("Contact URL:", this.qrValue);
|
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -771,14 +802,16 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
title: "Contact Exists",
|
title: "Contact Exists",
|
||||||
text: "This contact has already been added to your list.",
|
text: "This contact has already been added to your list.",
|
||||||
},
|
},
|
||||||
3000,
|
5000,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add new contact
|
// Add new contact
|
||||||
// @ts-expect-error because we're just using the value to store to the DB
|
// @ts-expect-error because we're just using the value to store to the DB
|
||||||
contact.contactMethods = JSON.stringify(contact.contactMethods);
|
contact.contactMethods = JSON.stringify(
|
||||||
|
parseJsonField(contact.contactMethods, []),
|
||||||
|
);
|
||||||
const { sql, params } = databaseUtil.generateInsertStatement(
|
const { sql, params } = databaseUtil.generateInsertStatement(
|
||||||
contact as unknown as Record<string, unknown>,
|
contact as unknown as Record<string, unknown>,
|
||||||
"contacts",
|
"contacts",
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2 h-10"
|
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2 h-10"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
class="px-4 rounded-r bg-green-200 border border-l-0 border-green-400"
|
class="px-4 rounded-r bg-green-200 border border-green-400"
|
||||||
@click="onClickNewContact()"
|
@click="onClickNewContact()"
|
||||||
>
|
>
|
||||||
<font-awesome icon="plus" class="fa-fw" />
|
<font-awesome icon="plus" class="fa-fw" />
|
||||||
@@ -86,8 +86,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="contacts.length > 0" class="flex justify-between">
|
<div v-if="contacts.length > 0" class="flex justify-between">
|
||||||
<div class="w-full text-left">
|
<div class="">
|
||||||
<div v-if="!showGiveNumbers">
|
<div v-if="!showGiveNumbers" class="flex items-center">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
:checked="contactsSelected.length === contacts.length"
|
:checked="contactsSelected.length === contacts.length"
|
||||||
@@ -101,52 +101,32 @@
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-if="!showGiveNumbers"
|
v-if="!showGiveNumbers"
|
||||||
href=""
|
:class="
|
||||||
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-3 px-3 py-1.5 rounded-md"
|
|
||||||
:style="
|
|
||||||
contactsSelected.length > 0
|
contactsSelected.length > 0
|
||||||
? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);'
|
? 'text-md bg-gradient-to-b from-blue-400 to-blue-700 ' +
|
||||||
: 'background-image: linear-gradient(to bottom, #94a3b8, #374151);'
|
'shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ' +
|
||||||
|
'ml-3 px-3 py-1.5 rounded-md cursor-pointer'
|
||||||
|
: 'text-md bg-gradient-to-b from-slate-400 to-slate-700 ' +
|
||||||
|
'shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-slate-300 ' +
|
||||||
|
'ml-3 px-3 py-1.5 rounded-md cursor-not-allowed'
|
||||||
"
|
"
|
||||||
data-testId="copySelectedContactsButtonTop"
|
data-testId="copySelectedContactsButtonTop"
|
||||||
@click="copySelectedContacts()"
|
@click="copySelectedContacts()"
|
||||||
>
|
>
|
||||||
Copy Selections
|
Copy
|
||||||
</button>
|
|
||||||
<button @click="showCopySelectionsInfo()">
|
|
||||||
<font-awesome
|
|
||||||
icon="circle-info"
|
|
||||||
class="text-xl text-blue-500 ml-4"
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
|
<font-awesome
|
||||||
|
icon="circle-info"
|
||||||
|
class="text-2xl text-blue-500 ml-2"
|
||||||
|
@click="showCopySelectionsInfo()"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full text-right">
|
<div class="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
href=""
|
v-if="showGiveNumbers"
|
||||||
class="text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
|
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
|
||||||
@click="toggleShowContactAmounts()"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
showGiveNumbers ? "Hide Hours, Offer, etc" : "See Hours, Offer, etc"
|
|
||||||
}}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="showGiveNumbers" class="flex justify-between mt-1">
|
|
||||||
<div class="w-full text-right">
|
|
||||||
In the following, only the most recent hours are included. To see more,
|
|
||||||
click
|
|
||||||
<span
|
|
||||||
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1 py-1 rounded-md"
|
|
||||||
>
|
|
||||||
<font-awesome icon="file-lines" class="fa-fw" />
|
|
||||||
</span>
|
|
||||||
<br />
|
|
||||||
<button
|
|
||||||
href=""
|
|
||||||
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md mt-1"
|
|
||||||
:class="showGiveAmountsClassNames()"
|
:class="showGiveAmountsClassNames()"
|
||||||
@click="toggleShowGiveTotals()"
|
@click="toggleShowGiveTotals()"
|
||||||
>
|
>
|
||||||
@@ -159,6 +139,24 @@
|
|||||||
}}
|
}}
|
||||||
<font-awesome icon="left-right" class="fa-fw" />
|
<font-awesome icon="left-right" class="fa-fw" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
|
||||||
|
@click="toggleShowContactAmounts()"
|
||||||
|
>
|
||||||
|
{{ showGiveNumbers ? "Hide Actions" : "See Actions" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="showGiveNumbers" class="my-3">
|
||||||
|
<div class="w-full text-center text-sm italic text-slate-600">
|
||||||
|
Only the most recent hours are included. <br />To see more, click
|
||||||
|
<span
|
||||||
|
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1 py-0.5 rounded"
|
||||||
|
>
|
||||||
|
<font-awesome icon="file-lines" class="text-xs fa-fw" />
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -166,7 +164,7 @@
|
|||||||
<ul
|
<ul
|
||||||
v-if="contacts.length > 0"
|
v-if="contacts.length > 0"
|
||||||
id="listContacts"
|
id="listContacts"
|
||||||
class="border-t border-slate-300 mt-1"
|
class="border-t border-slate-300 my-2"
|
||||||
>
|
>
|
||||||
<li
|
<li
|
||||||
v-for="contact in filteredContacts()"
|
v-for="contact in filteredContacts()"
|
||||||
@@ -174,125 +172,125 @@
|
|||||||
class="border-b border-slate-300 pt-1 pb-1"
|
class="border-b border-slate-300 pt-1 pb-1"
|
||||||
data-testId="contactListItem"
|
data-testId="contactListItem"
|
||||||
>
|
>
|
||||||
<div class="grow overflow-hidden">
|
<div class="flex items-center justify-between gap-3">
|
||||||
<div class="flex items-center justify-between gap-3">
|
<div class="flex overflow-hidden min-w-0 items-center gap-3">
|
||||||
<div class="flex items-center gap-3">
|
<input
|
||||||
<input
|
v-if="!showGiveNumbers"
|
||||||
v-if="!showGiveNumbers"
|
type="checkbox"
|
||||||
type="checkbox"
|
:checked="contactsSelected.includes(contact.did)"
|
||||||
:checked="contactsSelected.includes(contact.did)"
|
class="ml-2 h-6 w-6 flex-shrink-0"
|
||||||
class="ml-2 h-6 w-6 flex-shrink-0"
|
data-testId="contactCheckOne"
|
||||||
data-testId="contactCheckOne"
|
@click="
|
||||||
@click="
|
contactsSelected.includes(contact.did)
|
||||||
contactsSelected.includes(contact.did)
|
? contactsSelected.splice(
|
||||||
? contactsSelected.splice(
|
contactsSelected.indexOf(contact.did),
|
||||||
contactsSelected.indexOf(contact.did),
|
1,
|
||||||
1,
|
)
|
||||||
)
|
: contactsSelected.push(contact.did)
|
||||||
: contactsSelected.push(contact.did)
|
"
|
||||||
"
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
<EntityIcon
|
||||||
class="flex-shrink-0 w-12 h-12 flex items-center justify-center"
|
:contact="contact"
|
||||||
>
|
:icon-size="48"
|
||||||
<EntityIcon
|
class="shrink-0 align-text-bottom border border-slate-300 rounded cursor-pointer overflow-hidden"
|
||||||
:contact="contact"
|
@click="showLargeIdenticon = contact"
|
||||||
:icon-size="48"
|
/>
|
||||||
class="inline-block align-text-bottom border border-slate-300 rounded cursor-pointer overflow-hidden"
|
|
||||||
@click="showLargeIdenticon = contact"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 class="text-base font-semibold w-1/3 truncate flex-shrink-0">
|
<div class="overflow-hidden">
|
||||||
{{ contactNameNonBreakingSpace(contact.name) }}
|
<h2 class="text-base font-semibold truncate">
|
||||||
|
<router-link
|
||||||
|
:to="{
|
||||||
|
path: '/did/' + encodeURIComponent(contact.did),
|
||||||
|
}"
|
||||||
|
title="See more about this person"
|
||||||
|
>
|
||||||
|
{{ contactNameNonBreakingSpace(contact.name) }}
|
||||||
|
</router-link>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<span>
|
<div class="flex gap-1.5 items-center overflow-hidden">
|
||||||
<div class="flex gap-2 items-center">
|
<router-link
|
||||||
<router-link
|
:to="{
|
||||||
:to="{
|
path: '/did/' + encodeURIComponent(contact.did),
|
||||||
path: '/did/' + encodeURIComponent(contact.did),
|
}"
|
||||||
}"
|
title="See more about this person"
|
||||||
title="See more about this person"
|
>
|
||||||
>
|
<font-awesome
|
||||||
<font-awesome
|
icon="circle-info"
|
||||||
icon="circle-info"
|
class="text-base text-blue-500"
|
||||||
class="text-xl text-blue-500"
|
/>
|
||||||
/>
|
</router-link>
|
||||||
</router-link>
|
|
||||||
|
|
||||||
<span class="text-sm overflow-hidden">{{
|
<span class="text-xs truncate">{{ contact.did }}</span>
|
||||||
libsUtil.shortDid(contact.did)
|
</div>
|
||||||
}}</span>
|
<div class="text-sm">
|
||||||
</div>
|
{{ contact.notes }}
|
||||||
<div class="text-sm">
|
</div>
|
||||||
{{ contact.notes }}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
|
||||||
|
<div
|
||||||
|
v-if="showGiveNumbers && contact.did != activeDid"
|
||||||
|
class="flex gap-1.5 items-end"
|
||||||
|
>
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-xs leading-none mb-1">From/To</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<button
|
||||||
|
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2.5 py-1.5 rounded-l-md"
|
||||||
|
:title="givenToMeDescriptions[contact.did] || ''"
|
||||||
|
@click="confirmShowGiftedDialog(contact.did, activeDid)"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
/* eslint-disable prettier/prettier */
|
||||||
|
showGiveTotals
|
||||||
|
? ((givenToMeConfirmed[contact.did] || 0)
|
||||||
|
+ (givenToMeUnconfirmed[contact.did] || 0))
|
||||||
|
: showGiveConfirmed
|
||||||
|
? (givenToMeConfirmed[contact.did] || 0)
|
||||||
|
: (givenToMeUnconfirmed[contact.did] || 0)
|
||||||
|
/* eslint-enable prettier/prettier */
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2.5 py-1.5 rounded-r-md border-l"
|
||||||
|
:title="givenByMeDescriptions[contact.did] || ''"
|
||||||
|
@click="confirmShowGiftedDialog(activeDid, contact.did)"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
/* eslint-disable prettier/prettier */
|
||||||
|
showGiveTotals
|
||||||
|
? ((givenByMeConfirmed[contact.did] || 0)
|
||||||
|
+ (givenByMeUnconfirmed[contact.did] || 0))
|
||||||
|
: showGiveConfirmed
|
||||||
|
? (givenByMeConfirmed[contact.did] || 0)
|
||||||
|
: (givenByMeUnconfirmed[contact.did] || 0)
|
||||||
|
/* eslint-enable prettier/prettier */
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<button
|
||||||
v-if="showGiveNumbers && contact.did != activeDid"
|
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-md"
|
||||||
class="flex gap-2 items-center"
|
data-testId="offerButton"
|
||||||
|
@click="openOfferDialog(contact.did, contact.name)"
|
||||||
>
|
>
|
||||||
<button
|
Offer
|
||||||
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-l-md"
|
</button>
|
||||||
:title="givenToMeDescriptions[contact.did] || ''"
|
|
||||||
@click="confirmShowGiftedDialog(contact.did, activeDid)"
|
|
||||||
>
|
|
||||||
From:
|
|
||||||
<br />
|
|
||||||
{{
|
|
||||||
/* eslint-disable prettier/prettier */
|
|
||||||
showGiveTotals
|
|
||||||
? ((givenToMeConfirmed[contact.did] || 0)
|
|
||||||
+ (givenToMeUnconfirmed[contact.did] || 0))
|
|
||||||
: showGiveConfirmed
|
|
||||||
? (givenToMeConfirmed[contact.did] || 0)
|
|
||||||
: (givenToMeUnconfirmed[contact.did] || 0)
|
|
||||||
/* eslint-enable prettier/prettier */
|
|
||||||
}}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
<router-link
|
||||||
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white -ml-1.5 px-2 py-1.5 rounded-r-md border-l"
|
:to="{
|
||||||
:title="givenByMeDescriptions[contact.did] || ''"
|
name: 'contact-amounts',
|
||||||
@click="confirmShowGiftedDialog(activeDid, contact.did)"
|
query: { contactDid: contact.did },
|
||||||
>
|
}"
|
||||||
To:
|
class="text-sm bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-md"
|
||||||
<br />
|
title="See more given activity"
|
||||||
{{
|
>
|
||||||
/* eslint-disable prettier/prettier */
|
<font-awesome icon="file-lines" class="fa-fw" />
|
||||||
showGiveTotals
|
</router-link>
|
||||||
? ((givenByMeConfirmed[contact.did] || 0)
|
|
||||||
+ (givenByMeUnconfirmed[contact.did] || 0))
|
|
||||||
: showGiveConfirmed
|
|
||||||
? (givenByMeConfirmed[contact.did] || 0)
|
|
||||||
: (givenByMeUnconfirmed[contact.did] || 0)
|
|
||||||
/* eslint-enable prettier/prettier */
|
|
||||||
}}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-md border border-blue-400"
|
|
||||||
data-testId="offerButton"
|
|
||||||
@click="openOfferDialog(contact.did, contact.name)"
|
|
||||||
>
|
|
||||||
Offer
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<router-link
|
|
||||||
:to="{
|
|
||||||
name: 'contact-amounts',
|
|
||||||
query: { contactDid: contact.did },
|
|
||||||
}"
|
|
||||||
class="text-sm bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-md border border-slate-400"
|
|
||||||
title="See more given activity"
|
|
||||||
>
|
|
||||||
<font-awesome icon="file-lines" class="fa-fw" />
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
@@ -314,16 +312,18 @@
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
v-if="!showGiveNumbers"
|
v-if="!showGiveNumbers"
|
||||||
href=""
|
:class="
|
||||||
class="text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-3 px-3 py-1.5 rounded-md"
|
|
||||||
:style="
|
|
||||||
contactsSelected.length > 0
|
contactsSelected.length > 0
|
||||||
? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);'
|
? 'text-md bg-gradient-to-b from-blue-400 to-blue-700 ' +
|
||||||
: 'background-image: linear-gradient(to bottom, #94a3b8, #374151);'
|
'shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ' +
|
||||||
|
'ml-3 px-3 py-1.5 rounded-md cursor-pointer'
|
||||||
|
: 'text-md bg-gradient-to-b from-slate-400 to-slate-700 ' +
|
||||||
|
'shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-slate-300 ' +
|
||||||
|
'ml-3 px-3 py-1.5 rounded-md cursor-not-allowed'
|
||||||
"
|
"
|
||||||
@click="copySelectedContacts()"
|
@click="copySelectedContacts()"
|
||||||
>
|
>
|
||||||
Copy Selections
|
Copy
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -491,7 +491,7 @@ export default class ContactsView extends Vue {
|
|||||||
private async processContactJwt() {
|
private async processContactJwt() {
|
||||||
// handle a contact sent via URL
|
// handle a contact sent via URL
|
||||||
//
|
//
|
||||||
// For external links, use /contact-import/:jwt with a JWT that has an array of contacts
|
// For external links, use /deep-link/contact-import/:jwt with a JWT that has an array of contacts
|
||||||
// because that will do better error checking for things like missing data on iOS platforms.
|
// because that will do better error checking for things like missing data on iOS platforms.
|
||||||
const importedContactJwt = this.$route.query["contactJwt"] as string;
|
const importedContactJwt = this.$route.query["contactJwt"] as string;
|
||||||
if (importedContactJwt) {
|
if (importedContactJwt) {
|
||||||
@@ -542,7 +542,7 @@ export default class ContactsView extends Vue {
|
|||||||
if (response.status != 201) {
|
if (response.status != 201) {
|
||||||
throw { error: { response: response } };
|
throw { error: { response: response } };
|
||||||
}
|
}
|
||||||
await databaseUtil.updateAccountSettings(this.activeDid, {
|
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
|
||||||
isRegistered: true,
|
isRegistered: true,
|
||||||
});
|
});
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
@@ -617,7 +617,7 @@ export default class ContactsView extends Vue {
|
|||||||
title: "Error with Invite",
|
title: "Error with Invite",
|
||||||
text: message,
|
text: message,
|
||||||
},
|
},
|
||||||
5000,
|
-1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// if we're here, they haven't redirected anywhere, so we'll redirect here without a query parameter
|
// if we're here, they haven't redirected anywhere, so we'll redirect here without a query parameter
|
||||||
@@ -933,45 +933,9 @@ export default class ContactsView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async addContactFromEndorserMobileLine(
|
private async addContactFromEndorserMobileLine(
|
||||||
line: string,
|
lineRaw: string,
|
||||||
): Promise<IndexableType> {
|
): Promise<IndexableType> {
|
||||||
// Note that Endorser Mobile puts name first, then did, etc.
|
const newContact = libsUtil.csvLineToContact(lineRaw);
|
||||||
let name = line;
|
|
||||||
let did = "";
|
|
||||||
let publicKeyInput, seesMe, registered;
|
|
||||||
const commaPos1 = line.indexOf(",");
|
|
||||||
if (commaPos1 > -1) {
|
|
||||||
name = line.substring(0, commaPos1).trim();
|
|
||||||
did = line.substring(commaPos1 + 1).trim();
|
|
||||||
const commaPos2 = line.indexOf(",", commaPos1 + 1);
|
|
||||||
if (commaPos2 > -1) {
|
|
||||||
did = line.substring(commaPos1 + 1, commaPos2).trim();
|
|
||||||
publicKeyInput = line.substring(commaPos2 + 1).trim();
|
|
||||||
const commaPos3 = line.indexOf(",", commaPos2 + 1);
|
|
||||||
if (commaPos3 > -1) {
|
|
||||||
publicKeyInput = line.substring(commaPos2 + 1, commaPos3).trim();
|
|
||||||
seesMe = line.substring(commaPos3 + 1).trim() == "true";
|
|
||||||
const commaPos4 = line.indexOf(",", commaPos3 + 1);
|
|
||||||
if (commaPos4 > -1) {
|
|
||||||
seesMe = line.substring(commaPos3 + 1, commaPos4).trim() == "true";
|
|
||||||
registered = line.substring(commaPos4 + 1).trim() == "true";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// help with potential mistakes while this sharing requires copy-and-paste
|
|
||||||
let publicKeyBase64 = publicKeyInput;
|
|
||||||
if (publicKeyBase64 && /^[0-9A-Fa-f]{66}$/i.test(publicKeyBase64)) {
|
|
||||||
// it must be all hex (compressed public key), so convert
|
|
||||||
publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64");
|
|
||||||
}
|
|
||||||
const newContact = {
|
|
||||||
did,
|
|
||||||
name,
|
|
||||||
publicKeyBase64,
|
|
||||||
seesMe,
|
|
||||||
registered,
|
|
||||||
};
|
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
const { sql, params } = databaseUtil.generateInsertStatement(
|
const { sql, params } = databaseUtil.generateInsertStatement(
|
||||||
newContact as unknown as Record<string, unknown>,
|
newContact as unknown as Record<string, unknown>,
|
||||||
@@ -998,8 +962,6 @@ export default class ContactsView extends Vue {
|
|||||||
newContact as unknown as Record<string, unknown>,
|
newContact as unknown as Record<string, unknown>,
|
||||||
"contacts",
|
"contacts",
|
||||||
);
|
);
|
||||||
logger.error("sql", sql);
|
|
||||||
logger.error("params", params);
|
|
||||||
let contactPromise = platformService.dbExec(sql, params);
|
let contactPromise = platformService.dbExec(sql, params);
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
// @ts-expect-error since the result of this promise won't be used, and this will go away soon
|
// @ts-expect-error since the result of this promise won't be used, and this will go away soon
|
||||||
@@ -1160,7 +1122,7 @@ export default class ContactsView extends Vue {
|
|||||||
(regResult.error as string) ||
|
(regResult.error as string) ||
|
||||||
"Something went wrong during registration.",
|
"Something went wrong during registration.",
|
||||||
},
|
},
|
||||||
5000,
|
-1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1194,7 +1156,7 @@ export default class ContactsView extends Vue {
|
|||||||
title: "Registration Error",
|
title: "Registration Error",
|
||||||
text: userMessage,
|
text: userMessage,
|
||||||
},
|
},
|
||||||
5000,
|
-1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1215,7 +1177,6 @@ export default class ContactsView extends Vue {
|
|||||||
);
|
);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
//contact.seesMe = visibility; // why doesn't it affect the UI from here?
|
//contact.seesMe = visibility; // why doesn't it affect the UI from here?
|
||||||
//console.log("Set result & seesMe", result, contact.seesMe, contact.did);
|
|
||||||
if (showSuccessAlert) {
|
if (showSuccessAlert) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
@@ -1431,14 +1392,11 @@ export default class ContactsView extends Vue {
|
|||||||
}
|
}
|
||||||
return contact;
|
return contact;
|
||||||
});
|
});
|
||||||
// console.log(
|
|
||||||
// "Array of selected contacts:",
|
|
||||||
// JSON.stringify(selectedContacts),
|
|
||||||
// );
|
|
||||||
const contactsJwt = await createEndorserJwtForDid(this.activeDid, {
|
const contactsJwt = await createEndorserJwtForDid(this.activeDid, {
|
||||||
contacts: selectedContacts,
|
contacts: selectedContacts,
|
||||||
});
|
});
|
||||||
const contactsJwtUrl = APP_SERVER + "/contact-import/" + contactsJwt;
|
const contactsJwtUrl =
|
||||||
|
APP_SERVER + "/deep-link/contact-import/" + contactsJwt;
|
||||||
useClipboard()
|
useClipboard()
|
||||||
.copy(contactsJwtUrl)
|
.copy(contactsJwtUrl)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|||||||
@@ -66,9 +66,14 @@ const formattedPath = computed(() => {
|
|||||||
const path = originalPath.value.replace(/^\/+/, "");
|
const path = originalPath.value.replace(/^\/+/, "");
|
||||||
|
|
||||||
// Log for debugging
|
// Log for debugging
|
||||||
logger.log("Original Path:", originalPath.value);
|
logger.log(
|
||||||
logger.log("Route Params:", route.params);
|
"[DeepLinkError] Original Path:",
|
||||||
logger.log("Route Query:", route.query);
|
originalPath.value,
|
||||||
|
"Route Params:",
|
||||||
|
route.params,
|
||||||
|
"Route Query:",
|
||||||
|
route.query,
|
||||||
|
);
|
||||||
|
|
||||||
return path;
|
return path;
|
||||||
});
|
});
|
||||||
|
|||||||
227
src/views/DeepLinkRedirectView.vue
Normal file
227
src/views/DeepLinkRedirectView.vue
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
<template>
|
||||||
|
<!-- CONTENT -->
|
||||||
|
<section id="Content" class="relative w-[100vw] h-[100vh]">
|
||||||
|
<div
|
||||||
|
class="p-6 bg-white w-full max-w-[calc((100vh-env(safe-area-inset-top)-env(safe-area-inset-bottom))*0.4)] mx-auto"
|
||||||
|
>
|
||||||
|
<div class="mb-4">
|
||||||
|
<h1 class="text-xl text-center font-semibold relative mb-4">
|
||||||
|
Redirecting to Time Safari
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div v-if="destinationUrl" class="space-y-4">
|
||||||
|
<!-- Platform-specific messaging -->
|
||||||
|
<div class="text-center text-gray-600 mb-4">
|
||||||
|
<p v-if="isMobile">
|
||||||
|
{{
|
||||||
|
isIOS
|
||||||
|
? "Opening Time Safari app on your iPhone..."
|
||||||
|
: "Opening Time Safari app on your Android device..."
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
<p v-else>Opening Time Safari app...</p>
|
||||||
|
<p class="text-sm mt-2">
|
||||||
|
<span v-if="isMobile"
|
||||||
|
>If the app doesn't open automatically, use one of these
|
||||||
|
options:</span
|
||||||
|
>
|
||||||
|
<span v-else>Choose how you'd like to open this link:</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Deep Link Button -->
|
||||||
|
<div class="text-center">
|
||||||
|
<a
|
||||||
|
:href="deepLinkUrl || '#'"
|
||||||
|
class="inline-block bg-blue-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors"
|
||||||
|
@click="handleDeepLinkClick"
|
||||||
|
>
|
||||||
|
<span v-if="isMobile">Open in Time Safari App</span>
|
||||||
|
<span v-else>Try Opening in Time Safari App</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Web Fallback Link -->
|
||||||
|
<div class="text-center">
|
||||||
|
<a
|
||||||
|
:href="webUrl || '#'"
|
||||||
|
target="_blank"
|
||||||
|
class="inline-block bg-gray-600 text-white px-6 py-3 rounded-lg font-medium hover:bg-gray-700 transition-colors"
|
||||||
|
@click="handleWebFallbackClick"
|
||||||
|
>
|
||||||
|
<span v-if="isMobile">Open in Web Browser Instead</span>
|
||||||
|
<span v-else>Open in Web Browser</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Manual Instructions -->
|
||||||
|
<div class="text-center text-sm text-gray-500 mt-4">
|
||||||
|
<p v-if="isMobile">
|
||||||
|
Or manually open:
|
||||||
|
<code class="bg-gray-100 px-2 py-1 rounded">{{
|
||||||
|
deepLinkUrl
|
||||||
|
}}</code>
|
||||||
|
</p>
|
||||||
|
<p v-else>
|
||||||
|
If you have the Time Safari app installed, you can also copy this
|
||||||
|
link:
|
||||||
|
<code class="bg-gray-100 px-2 py-1 rounded">{{
|
||||||
|
deepLinkUrl
|
||||||
|
}}</code>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Platform info for debugging -->
|
||||||
|
<div
|
||||||
|
v-if="isDevelopment"
|
||||||
|
class="text-center text-xs text-gray-400 mt-4"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Platform: {{ isMobile ? (isIOS ? "iOS" : "Android") : "Desktop" }}
|
||||||
|
</p>
|
||||||
|
<p>User Agent: {{ userAgent.substring(0, 50) }}...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="pageError" class="text-center text-red-500 mb-4">
|
||||||
|
{{ pageError }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="text-center text-gray-600">
|
||||||
|
<p>Processing redirect...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||||
|
|
||||||
|
import { APP_SERVER } from "@/constants/app";
|
||||||
|
import { logger } from "@/utils/logger";
|
||||||
|
import { errorStringForLog } from "@/libs/endorserServer";
|
||||||
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||||
|
|
||||||
|
@Component({})
|
||||||
|
export default class DeepLinkRedirectView extends Vue {
|
||||||
|
$router!: Router;
|
||||||
|
$route!: RouteLocationNormalizedLoaded;
|
||||||
|
pageError: string | null = null;
|
||||||
|
destinationUrl: string | null = null; // full path after "/deep-link/"
|
||||||
|
deepLinkUrl: string | null = null; // mobile link starting "timesafari://"
|
||||||
|
webUrl: string | null = null; // web link, eg "https://timesafari.app/..."
|
||||||
|
isDevelopment: boolean = false;
|
||||||
|
userAgent: string = "";
|
||||||
|
private platformService = PlatformServiceFactory.getInstance();
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
// Get the path from the route parameter (catch-all parameter)
|
||||||
|
const pathParam = this.$route.params.path;
|
||||||
|
|
||||||
|
// If pathParam is an array (catch-all parameter), join it
|
||||||
|
const fullPath = Array.isArray(pathParam) ? pathParam.join("/") : pathParam;
|
||||||
|
|
||||||
|
// Get query parameters from the route
|
||||||
|
const queryParams = this.$route.query;
|
||||||
|
|
||||||
|
// Build query string if there are query parameters
|
||||||
|
let queryString = "";
|
||||||
|
if (Object.keys(queryParams).length > 0) {
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
Object.entries(queryParams).forEach(([key, value]) => {
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
const stringValue = Array.isArray(value) ? value[0] : value;
|
||||||
|
if (stringValue !== null && stringValue !== undefined) {
|
||||||
|
searchParams.append(key, stringValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
queryString = "?" + searchParams.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine path with query parameters
|
||||||
|
const fullPathWithQuery = fullPath + queryString;
|
||||||
|
|
||||||
|
this.destinationUrl = fullPathWithQuery;
|
||||||
|
this.deepLinkUrl = `timesafari://${fullPathWithQuery}`;
|
||||||
|
this.webUrl = `${APP_SERVER}/${fullPathWithQuery}`;
|
||||||
|
|
||||||
|
this.isDevelopment = process.env.NODE_ENV !== "production";
|
||||||
|
this.userAgent = navigator.userAgent;
|
||||||
|
|
||||||
|
this.openDeepLink();
|
||||||
|
}
|
||||||
|
|
||||||
|
private openDeepLink() {
|
||||||
|
if (!this.deepLinkUrl || !this.webUrl) {
|
||||||
|
this.pageError =
|
||||||
|
"No deep link was provided. Check the URL and try again.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// For mobile, try the deep link URL; for desktop, use the web URL
|
||||||
|
const redirectUrl = this.isMobile ? this.deepLinkUrl : this.webUrl;
|
||||||
|
|
||||||
|
// Method 1: Try window.location.href (works on most browsers)
|
||||||
|
window.location.href = redirectUrl;
|
||||||
|
|
||||||
|
// Method 2: Fallback - create and click a link element
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = redirectUrl;
|
||||||
|
link.style.display = "none";
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
"Fallback deep link failed: " + errorStringForLog(error),
|
||||||
|
);
|
||||||
|
this.pageError =
|
||||||
|
"Redirecting to the Time Safari app failed. Please use a manual option below.";
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Deep link redirect failed: " + errorStringForLog(error));
|
||||||
|
this.pageError =
|
||||||
|
"Unable to open the Time Safari app. Please use a manual option below.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleDeepLinkClick(event: Event) {
|
||||||
|
if (!this.deepLinkUrl) return;
|
||||||
|
|
||||||
|
// Prevent default to handle the click manually
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
this.openDeepLink();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleWebFallbackClick(event: Event) {
|
||||||
|
if (!this.webUrl) return;
|
||||||
|
|
||||||
|
// Get platform capabilities
|
||||||
|
const capabilities = this.platformService.getCapabilities();
|
||||||
|
|
||||||
|
// For mobile, try to open in a new tab/window
|
||||||
|
if (capabilities.isMobile) {
|
||||||
|
event.preventDefault();
|
||||||
|
window.open(this.webUrl, "_blank");
|
||||||
|
}
|
||||||
|
// For desktop, let the default behavior happen (opens in same tab)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Computed properties for template
|
||||||
|
get isMobile(): boolean {
|
||||||
|
return this.platformService.getCapabilities().isMobile;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isIOS(): boolean {
|
||||||
|
return this.platformService.getCapabilities().isIOS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -523,9 +523,7 @@ export default class DiscoverView extends Vue {
|
|||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.error("Error with search all:", e);
|
logger.error("Error with search all: " + errorStringForLog(e));
|
||||||
// this sometimes gives different information
|
|
||||||
logger.error("Error with search all (error added): " + e);
|
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -617,7 +615,7 @@ export default class DiscoverView extends Vue {
|
|||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.error("Error with search local:", e);
|
logger.error("Error with search local: " + errorStringForLog(e));
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -788,7 +786,7 @@ export default class DiscoverView extends Vue {
|
|||||||
const route = {
|
const route = {
|
||||||
path: this.isProjectsActive
|
path: this.isProjectsActive
|
||||||
? "/project/" + encodeURIComponent(id)
|
? "/project/" + encodeURIComponent(id)
|
||||||
: "/userProfile/" + encodeURIComponent(id),
|
: "/user-profile/" + encodeURIComponent(id),
|
||||||
};
|
};
|
||||||
this.$router.push(route);
|
this.$router.push(route);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -825,11 +825,8 @@ export default class GiftedDetails extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (!result.success) {
|
||||||
result.type === "error" ||
|
const errorMessage = result.error;
|
||||||
this.isGiveCreationError(result.response)
|
|
||||||
) {
|
|
||||||
const errorMessage = this.getGiveCreationErrorMessage(result);
|
|
||||||
logger.error("Error with give creation result:", result);
|
logger.error("Error with give creation result:", result);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
@@ -902,28 +899,6 @@ export default class GiftedDetails extends Vue {
|
|||||||
|
|
||||||
// Helper functions for readability
|
// Helper functions for readability
|
||||||
|
|
||||||
/**
|
|
||||||
* @param result response "data" from the server
|
|
||||||
* @returns true if the result indicates an error
|
|
||||||
*/
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
isGiveCreationError(result: any) {
|
|
||||||
return result.status !== 201 || result.data?.error;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
|
|
||||||
* @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() {
|
explainData() {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -24,11 +24,11 @@
|
|||||||
<!-- eslint-disable prettier/prettier max-len -->
|
<!-- eslint-disable prettier/prettier max-len -->
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
This app focuses on gifts & gratitude, using them to build cool things together with your network.
|
This app focuses on raw gratitude, using it to build cool things together with your network.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="ml-4">
|
<p class="ml-4">
|
||||||
If you'd like to see the page-by-page help,
|
If you'd like to see the page-by-page help again,
|
||||||
<span
|
<span
|
||||||
class="text-blue-500 cursor-pointer"
|
class="text-blue-500 cursor-pointer"
|
||||||
@click="unsetFinishedOnboarding()"
|
@click="unsetFinishedOnboarding()"
|
||||||
@@ -37,14 +37,16 @@
|
|||||||
|
|
||||||
<h2 class="text-xl font-semibold">What is the idea here?</h2>
|
<h2 class="text-xl font-semibold">What is the idea here?</h2>
|
||||||
<p>
|
<p>
|
||||||
We are building networks of people who want to grow good society from the ground up, using modern
|
We are building networks of people who want to grow good society from the ground up, using
|
||||||
technology that connects people peer-to-peer.
|
modern technology that connects people peer-to-peer.
|
||||||
First of all, let's showcase gratitude: see what people have given, and recognize
|
First of all, let's showcase gratitude: see what people have given, and recognize gifts
|
||||||
gifts you've seen. This is done in a way that leaves a permanent record -- one that
|
you've seen. This is done in a way that leaves a permanent record -- one that provably
|
||||||
came from you, and one that the recipient can prove it was for them. This can be
|
came from you, and one that the recipient can prove they were mentioned.
|
||||||
personally gratifying, but it extends to broader work: volunteers get
|
This can be personally gratifying, but it extends to broader work: volunteers get
|
||||||
confirmation of activity, and they can selectively show off their contributions
|
confirmation of activity, and they can selectively show off their contributions and
|
||||||
and network.
|
network.
|
||||||
|
This is a way to build trust and reputation. It's a way to build a network of people who
|
||||||
|
are willing to help each other.
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-2">
|
<p class="mt-2">
|
||||||
With this, you highlight giving and you also offer help --
|
With this, you highlight giving and you also offer help --
|
||||||
@@ -555,9 +557,6 @@
|
|||||||
initiative.
|
initiative.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">What app version is this?</h2>
|
|
||||||
<p>{{ package.version }} ({{ commitHash }})</p>
|
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">
|
<h2 class="text-xl font-semibold">
|
||||||
I have other questions or feedback, like getting a new profile or removing my data or requesting an improvement.
|
I have other questions or feedback, like getting a new profile or removing my data or requesting an improvement.
|
||||||
</h2>
|
</h2>
|
||||||
@@ -567,6 +566,28 @@
|
|||||||
>info@TimeSafari.app</a
|
>info@TimeSafari.app</a
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<h2 class="text-xl font-semibold">What app version is this?</h2>
|
||||||
|
<p>{{ package.version }} ({{ commitHash }})</p>
|
||||||
|
|
||||||
|
<div v-if="Capacitor.isNativePlatform()">
|
||||||
|
<h2 class="text-xl font-semibold">
|
||||||
|
Do I have the latest version?
|
||||||
|
</h2>
|
||||||
|
<p v-if="Capacitor.getPlatform() === 'ios'">
|
||||||
|
<a href="https://apps.apple.com/us/app/time-safari/id6742664907" target="_blank" class="text-blue-500">
|
||||||
|
Check the App Store.
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p v-else-if="Capacitor.getPlatform() === 'android'">
|
||||||
|
<a href="https://timesafari.app/app.apk" target="_blank" class="text-blue-500">
|
||||||
|
Download the latest APK to see.
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p v-else>
|
||||||
|
Sorry, your platform of '{{ Capacitor.getPlatform() }}' is not recognized.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- eslint enable -->
|
<!-- eslint enable -->
|
||||||
</section>
|
</section>
|
||||||
@@ -603,6 +624,7 @@ export default class HelpView extends Vue {
|
|||||||
showVerifiable = false;
|
showVerifiable = false;
|
||||||
|
|
||||||
APP_SERVER = APP_SERVER;
|
APP_SERVER = APP_SERVER;
|
||||||
|
Capacitor = Capacitor;
|
||||||
|
|
||||||
// Ideally, we put no functionality in here, especially in the setup,
|
// Ideally, we put no functionality in here, especially in the setup,
|
||||||
// because we never want this page to have a chance of throwing an error.
|
// because we never want this page to have a chance of throwing an error.
|
||||||
@@ -622,7 +644,7 @@ export default class HelpView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (settings.activeDid) {
|
if (settings.activeDid) {
|
||||||
await databaseUtil.updateAccountSettings(settings.activeDid, {
|
await databaseUtil.updateDidSpecificSettings(settings.activeDid, {
|
||||||
finishedOnboarding: false,
|
finishedOnboarding: false,
|
||||||
});
|
});
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
|
|||||||
@@ -519,7 +519,6 @@ export default class HomeView extends Vue {
|
|||||||
// Retrieve DIDs with better error handling
|
// Retrieve DIDs with better error handling
|
||||||
try {
|
try {
|
||||||
this.allMyDids = await retrieveAccountDids();
|
this.allMyDids = await retrieveAccountDids();
|
||||||
logConsoleAndDb(`[HomeView] Retrieved ${this.allMyDids.length} DIDs`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb(`[HomeView] Failed to retrieve DIDs: ${error}`, true);
|
logConsoleAndDb(`[HomeView] Failed to retrieve DIDs: ${error}`, true);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -552,9 +551,6 @@ export default class HomeView extends Vue {
|
|||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
settings = await retrieveSettingsForActiveAccount();
|
settings = await retrieveSettingsForActiveAccount();
|
||||||
}
|
}
|
||||||
logConsoleAndDb(
|
|
||||||
`[HomeView] Retrieved settings for ${settings.activeDid || "no active DID"}`,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
`[HomeView] Failed to retrieve settings: ${error}`,
|
`[HomeView] Failed to retrieve settings: ${error}`,
|
||||||
@@ -581,9 +577,6 @@ export default class HomeView extends Vue {
|
|||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
this.allContacts = await db.contacts.toArray();
|
this.allContacts = await db.contacts.toArray();
|
||||||
}
|
}
|
||||||
logConsoleAndDb(
|
|
||||||
`[HomeView] Retrieved ${this.allContacts.length} contacts`,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
`[HomeView] Failed to retrieve contacts: ${error}`,
|
`[HomeView] Failed to retrieve contacts: ${error}`,
|
||||||
@@ -630,7 +623,7 @@ export default class HomeView extends Vue {
|
|||||||
this.activeDid,
|
this.activeDid,
|
||||||
);
|
);
|
||||||
if (resp.status === 200) {
|
if (resp.status === 200) {
|
||||||
await databaseUtil.updateAccountSettings(this.activeDid, {
|
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
|
||||||
isRegistered: true,
|
isRegistered: true,
|
||||||
...(await databaseUtil.retrieveSettingsForActiveAccount()),
|
...(await databaseUtil.retrieveSettingsForActiveAccount()),
|
||||||
});
|
});
|
||||||
@@ -641,9 +634,6 @@ export default class HomeView extends Vue {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.isRegistered = true;
|
this.isRegistered = true;
|
||||||
logConsoleAndDb(
|
|
||||||
`[HomeView] User ${this.activeDid} is now registered`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
@@ -685,11 +675,6 @@ export default class HomeView extends Vue {
|
|||||||
this.newOffersToUserHitLimit = offersToUser.hitLimit;
|
this.newOffersToUserHitLimit = offersToUser.hitLimit;
|
||||||
this.numNewOffersToUserProjects = offersToProjects.data.length;
|
this.numNewOffersToUserProjects = offersToProjects.data.length;
|
||||||
this.newOffersToUserProjectsHitLimit = offersToProjects.hitLimit;
|
this.newOffersToUserProjectsHitLimit = offersToProjects.hitLimit;
|
||||||
|
|
||||||
logConsoleAndDb(
|
|
||||||
`[HomeView] Retrieved ${this.numNewOffersToUser} user offers and ` +
|
|
||||||
`${this.numNewOffersToUserProjects} project offers`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
@@ -785,7 +770,7 @@ export default class HomeView extends Vue {
|
|||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
settings = await retrieveSettingsForActiveAccount();
|
settings = await retrieveSettingsForActiveAccount();
|
||||||
}
|
}
|
||||||
await databaseUtil.updateAccountSettings(this.activeDid, {
|
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
|
||||||
apiServer: this.apiServer,
|
apiServer: this.apiServer,
|
||||||
isRegistered: true,
|
isRegistered: true,
|
||||||
...settings,
|
...settings,
|
||||||
@@ -1843,7 +1828,7 @@ export default class HomeView extends Vue {
|
|||||||
this.axios,
|
this.axios,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.type === "success") {
|
if (result.success) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
|
|||||||
@@ -79,9 +79,14 @@ import {
|
|||||||
newIdentifier,
|
newIdentifier,
|
||||||
nextDerivationPath,
|
nextDerivationPath,
|
||||||
} from "../libs/crypto";
|
} from "../libs/crypto";
|
||||||
import { accountsDBPromise, db } from "../db/index";
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
|
import { db } from "../db/index";
|
||||||
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
||||||
import { retrieveAllAccountsMetadata, retrieveFullyDecryptedAccount, saveNewIdentity } from "../libs/util";
|
import {
|
||||||
|
retrieveAllAccountsMetadata,
|
||||||
|
retrieveFullyDecryptedAccount,
|
||||||
|
saveNewIdentity,
|
||||||
|
} from "../libs/util";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { Account, AccountEncrypted } from "../db/tables/accounts";
|
import { Account, AccountEncrypted } from "../db/tables/accounts";
|
||||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||||
@@ -100,13 +105,20 @@ export default class ImportAccountView extends Vue {
|
|||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
const accounts: AccountEncrypted[] = await retrieveAllAccountsMetadata();
|
const accounts: AccountEncrypted[] = await retrieveAllAccountsMetadata();
|
||||||
const decryptedAccounts: (Account | undefined)[] = await Promise.all(accounts.map(async (account) => {
|
const decryptedAccounts: (Account | undefined)[] = await Promise.all(
|
||||||
return retrieveFullyDecryptedAccount(account.did);
|
accounts.map(async (account) => {
|
||||||
}));
|
return retrieveFullyDecryptedAccount(account.did);
|
||||||
const filteredDecryptedAccounts: Account[] = decryptedAccounts.filter((account) => account !== undefined);
|
}),
|
||||||
|
);
|
||||||
|
const filteredDecryptedAccounts: Account[] = decryptedAccounts.filter(
|
||||||
|
(account) => account !== undefined,
|
||||||
|
);
|
||||||
|
|
||||||
// group by account.mnemonic
|
// group by account.mnemonic
|
||||||
const groupedAccounts: Record<string, Account[]> = R.groupBy((a) => a.mnemonic || "", filteredDecryptedAccounts) as Record<string, Account[]>;
|
const groupedAccounts: Record<string, Account[]> = R.groupBy(
|
||||||
|
(a) => a.mnemonic || "",
|
||||||
|
filteredDecryptedAccounts,
|
||||||
|
) as Record<string, Account[]>;
|
||||||
|
|
||||||
this.didArrays = groupedAccounts;
|
this.didArrays = groupedAccounts;
|
||||||
if (Object.keys(this.didArrays).length > 0) {
|
if (Object.keys(this.didArrays).length > 0) {
|
||||||
@@ -125,10 +137,13 @@ export default class ImportAccountView extends Vue {
|
|||||||
public async incrementDerivation() {
|
public async incrementDerivation() {
|
||||||
// find the maximum derivation path for the selected DIDs
|
// find the maximum derivation path for the selected DIDs
|
||||||
const selectedArray: Array<Account> =
|
const selectedArray: Array<Account> =
|
||||||
Object.values(this.didArrays).find((dids) => dids[0].did === this.selectedArrayFirstDid) ||
|
Object.values(this.didArrays).find(
|
||||||
[];
|
(dids) => dids[0].did === this.selectedArrayFirstDid,
|
||||||
|
) || [];
|
||||||
// extract the derivationPath array and sort it
|
// extract the derivationPath array and sort it
|
||||||
const derivationPaths = selectedArray.map((account) => account.derivationPath);
|
const derivationPaths = selectedArray.map(
|
||||||
|
(account) => account.derivationPath,
|
||||||
|
);
|
||||||
derivationPaths.sort((a, b) => {
|
derivationPaths.sort((a, b) => {
|
||||||
const aParts = a?.split("/");
|
const aParts = a?.split("/");
|
||||||
const aLast = aParts?.[aParts.length - 1];
|
const aLast = aParts?.[aParts.length - 1];
|
||||||
@@ -137,7 +152,9 @@ export default class ImportAccountView extends Vue {
|
|||||||
return parseInt(aLast || "0") - parseInt(bLast || "0");
|
return parseInt(aLast || "0") - parseInt(bLast || "0");
|
||||||
});
|
});
|
||||||
// we're sure there's at least one
|
// we're sure there's at least one
|
||||||
const maxDerivPath: string = derivationPaths[derivationPaths.length - 1] as string;
|
const maxDerivPath: string = derivationPaths[
|
||||||
|
derivationPaths.length - 1
|
||||||
|
] as string;
|
||||||
|
|
||||||
const newDerivPath = nextDerivationPath(maxDerivPath);
|
const newDerivPath = nextDerivationPath(maxDerivPath);
|
||||||
|
|
||||||
@@ -148,23 +165,15 @@ export default class ImportAccountView extends Vue {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await saveNewIdentity(newId, mne, newDerivPath);
|
await saveNewIdentity(newId, mne, newDerivPath);
|
||||||
if (USE_DEXIE_DB) {
|
|
||||||
const accountsDB = await accountsDBPromise;
|
|
||||||
await accountsDB.accounts.add({
|
|
||||||
dateCreated: new Date().toISOString(),
|
|
||||||
derivationPath: newDerivPath,
|
|
||||||
did: newId.did,
|
|
||||||
identity: JSON.stringify(newId),
|
|
||||||
mnemonic: mne,
|
|
||||||
publicKeyHex: newId.keys[0].publicKeyHex,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// record that as the active DID
|
// record that as the active DID
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
await platformService.dbExec("UPDATE settings SET activeDid = ?", [
|
await platformService.dbExec("UPDATE settings SET activeDid = ?", [
|
||||||
newId.did,
|
newId.did,
|
||||||
]);
|
]);
|
||||||
|
await databaseUtil.updateDidSpecificSettings(newId.did, {
|
||||||
|
isRegistered: false,
|
||||||
|
});
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
activeDid: newId.did,
|
activeDid: newId.did,
|
||||||
|
|||||||
@@ -83,7 +83,7 @@
|
|||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
class="text-center text-slate-500 cursor-pointer"
|
class="text-center text-slate-500 cursor-pointer"
|
||||||
:title="inviteLink(invite.jwt)"
|
:title="invite.inviteIdentifier"
|
||||||
@click="
|
@click="
|
||||||
showInvite(
|
showInvite(
|
||||||
invite.inviteIdentifier,
|
invite.inviteIdentifier,
|
||||||
@@ -241,7 +241,7 @@ export default class InviteOneView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inviteLink(jwt: string): string {
|
inviteLink(jwt: string): string {
|
||||||
return APP_SERVER + "/invite-one-accept/" + jwt;
|
return APP_SERVER + "/deep-link/invite-one-accept/" + jwt;
|
||||||
}
|
}
|
||||||
|
|
||||||
copyInviteAndNotify(inviteId: string, jwt: string) {
|
copyInviteAndNotify(inviteId: string, jwt: string) {
|
||||||
@@ -324,7 +324,7 @@ export default class InviteOneView extends Vue {
|
|||||||
);
|
);
|
||||||
await axios.post(
|
await axios.post(
|
||||||
this.apiServer + "/api/userUtil/invite",
|
this.apiServer + "/api/userUtil/invite",
|
||||||
{ inviteIdentifier, inviteJwt, notes, expiresAt },
|
{ inviteJwt, notes, expiresAt },
|
||||||
{ headers },
|
{ headers },
|
||||||
);
|
);
|
||||||
const newInvite = {
|
const newInvite = {
|
||||||
|
|||||||
@@ -257,7 +257,7 @@ export default class NewActivityView extends Vue {
|
|||||||
async expandOffersToUserAndMarkRead() {
|
async expandOffersToUserAndMarkRead() {
|
||||||
this.showOffersDetails = !this.showOffersDetails;
|
this.showOffersDetails = !this.showOffersDetails;
|
||||||
if (this.showOffersDetails) {
|
if (this.showOffersDetails) {
|
||||||
await databaseUtil.updateAccountSettings(this.activeDid, {
|
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
|
||||||
lastAckedOfferToUserJwtId: this.newOffersToUser[0].jwtId,
|
lastAckedOfferToUserJwtId: this.newOffersToUser[0].jwtId,
|
||||||
});
|
});
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
@@ -285,7 +285,7 @@ export default class NewActivityView extends Vue {
|
|||||||
);
|
);
|
||||||
if (index !== -1 && index < this.newOffersToUser.length - 1) {
|
if (index !== -1 && index < this.newOffersToUser.length - 1) {
|
||||||
// Set to the next offer's jwtId
|
// Set to the next offer's jwtId
|
||||||
await databaseUtil.updateAccountSettings(this.activeDid, {
|
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
|
||||||
lastAckedOfferToUserJwtId: this.newOffersToUser[index + 1].jwtId,
|
lastAckedOfferToUserJwtId: this.newOffersToUser[index + 1].jwtId,
|
||||||
});
|
});
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
@@ -295,7 +295,7 @@ export default class NewActivityView extends Vue {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// it's the last entry (or not found), so just keep it the same
|
// it's the last entry (or not found), so just keep it the same
|
||||||
await databaseUtil.updateAccountSettings(this.activeDid, {
|
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
|
||||||
lastAckedOfferToUserJwtId: this.lastAckedOfferToUserJwtId,
|
lastAckedOfferToUserJwtId: this.lastAckedOfferToUserJwtId,
|
||||||
});
|
});
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
@@ -319,7 +319,7 @@ export default class NewActivityView extends Vue {
|
|||||||
this.showOffersToUserProjectsDetails =
|
this.showOffersToUserProjectsDetails =
|
||||||
!this.showOffersToUserProjectsDetails;
|
!this.showOffersToUserProjectsDetails;
|
||||||
if (this.showOffersToUserProjectsDetails) {
|
if (this.showOffersToUserProjectsDetails) {
|
||||||
await databaseUtil.updateAccountSettings(this.activeDid, {
|
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
|
||||||
lastAckedOfferToUserProjectsJwtId:
|
lastAckedOfferToUserProjectsJwtId:
|
||||||
this.newOffersToUserProjects[0].jwtId,
|
this.newOffersToUserProjects[0].jwtId,
|
||||||
});
|
});
|
||||||
@@ -349,7 +349,7 @@ export default class NewActivityView extends Vue {
|
|||||||
);
|
);
|
||||||
if (index !== -1 && index < this.newOffersToUserProjects.length - 1) {
|
if (index !== -1 && index < this.newOffersToUserProjects.length - 1) {
|
||||||
// Set to the next offer's jwtId
|
// Set to the next offer's jwtId
|
||||||
await databaseUtil.updateAccountSettings(this.activeDid, {
|
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
|
||||||
lastAckedOfferToUserProjectsJwtId:
|
lastAckedOfferToUserProjectsJwtId:
|
||||||
this.newOffersToUserProjects[index + 1].jwtId,
|
this.newOffersToUserProjects[index + 1].jwtId,
|
||||||
});
|
});
|
||||||
@@ -361,7 +361,7 @@ export default class NewActivityView extends Vue {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// it's the last entry (or not found), so just keep it the same
|
// it's the last entry (or not found), so just keep it the same
|
||||||
await databaseUtil.updateAccountSettings(this.activeDid, {
|
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
|
||||||
lastAckedOfferToUserProjectsJwtId:
|
lastAckedOfferToUserProjectsJwtId:
|
||||||
this.lastAckedOfferToUserProjectsJwtId,
|
this.lastAckedOfferToUserProjectsJwtId,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -720,7 +720,7 @@ export default class OnboardMeetingView extends Vue {
|
|||||||
|
|
||||||
onboardMeetingMembersLink(): string {
|
onboardMeetingMembersLink(): string {
|
||||||
if (this.currentMeeting) {
|
if (this.currentMeeting) {
|
||||||
return `${APP_SERVER}/onboard-meeting-members/${this.currentMeeting?.groupId}?password=${encodeURIComponent(
|
return `${APP_SERVER}/deep-link/onboard-meeting-members/${this.currentMeeting?.groupId}?password=${encodeURIComponent(
|
||||||
this.currentMeeting?.password || "",
|
this.currentMeeting?.password || "",
|
||||||
)}`;
|
)}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,12 @@
|
|||||||
>
|
>
|
||||||
<font-awesome icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
|
<font-awesome icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
|
||||||
</button>
|
</button>
|
||||||
|
<button title="Copy Link to Project" @click="onCopyLinkClick()">
|
||||||
|
<font-awesome
|
||||||
|
icon="link"
|
||||||
|
class="text-sm text-slate-500 ml-2 mb-1"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -52,19 +58,28 @@
|
|||||||
icon="user"
|
icon="user"
|
||||||
class="fa-fw text-slate-400"
|
class="fa-fw text-slate-400"
|
||||||
></font-awesome>
|
></font-awesome>
|
||||||
{{ issuerInfoObject?.displayName }}
|
<span class="truncate inline-block max-w-[calc(100%-2rem)]">
|
||||||
<span v-if="!serverUtil.isEmptyOrHiddenDid(issuer)">
|
{{ issuerInfoObject?.displayName }}
|
||||||
<a
|
</span>
|
||||||
:href="`/did/${issuer}`"
|
|
||||||
class="text-blue-500"
|
<span
|
||||||
|
v-if="!serverUtil.isHiddenDid(issuer)"
|
||||||
|
class="inline-flex items-center"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
:to="{
|
||||||
|
path: '/did/' + encodeURIComponent(issuer),
|
||||||
|
}"
|
||||||
|
class="text-blue-500 ml-1"
|
||||||
|
title="See more about this person"
|
||||||
>
|
>
|
||||||
<font-awesome
|
<font-awesome
|
||||||
icon="arrow-up-right-from-square"
|
icon="arrow-up-right-from-square"
|
||||||
class="fa-fw"
|
class="fa-fw"
|
||||||
/>
|
/>
|
||||||
</a>
|
</router-link>
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="serverUtil.isHiddenDid(issuer)">
|
<span v-if="serverUtil.isHiddenDid(issuer)" class="ml-1">
|
||||||
<font-awesome
|
<font-awesome
|
||||||
icon="info-circle"
|
icon="info-circle"
|
||||||
class="fa-fw text-blue-500 cursor-pointer"
|
class="fa-fw text-blue-500 cursor-pointer"
|
||||||
@@ -108,7 +123,7 @@
|
|||||||
class="fa-fw text-slate-400"
|
class="fa-fw text-slate-400"
|
||||||
></font-awesome>
|
></font-awesome>
|
||||||
<a
|
<a
|
||||||
:href="addScheme(url)"
|
:href="ensureScheme(url)"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="underline text-blue-500"
|
class="underline text-blue-500"
|
||||||
>
|
>
|
||||||
@@ -627,7 +642,7 @@ import TopMessage from "../components/TopMessage.vue";
|
|||||||
import QuickNav from "../components/QuickNav.vue";
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
import EntityIcon from "../components/EntityIcon.vue";
|
import EntityIcon from "../components/EntityIcon.vue";
|
||||||
import ProjectIcon from "../components/ProjectIcon.vue";
|
import ProjectIcon from "../components/ProjectIcon.vue";
|
||||||
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
import { APP_SERVER, NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||||
import * as databaseUtil from "../db/databaseUtil";
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
import {
|
import {
|
||||||
db,
|
db,
|
||||||
@@ -641,6 +656,7 @@ import { retrieveAccountDids } from "../libs/util";
|
|||||||
import HiddenDidDialog from "../components/HiddenDidDialog.vue";
|
import HiddenDidDialog from "../components/HiddenDidDialog.vue";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||||
|
import { useClipboard } from "@vueuse/core";
|
||||||
/**
|
/**
|
||||||
* Project View Component
|
* Project View Component
|
||||||
* @author Matthew Raymer
|
* @author Matthew Raymer
|
||||||
@@ -837,6 +853,28 @@ export default class ProjectViewView extends Vue {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onCopyLinkClick() {
|
||||||
|
const shortestProjectId = this.projectId.startsWith(
|
||||||
|
serverUtil.ENDORSER_CH_HANDLE_PREFIX,
|
||||||
|
)
|
||||||
|
? this.projectId.substring(serverUtil.ENDORSER_CH_HANDLE_PREFIX.length)
|
||||||
|
: this.projectId;
|
||||||
|
const deepLink = `${APP_SERVER}/deep-link/project/${shortestProjectId}`;
|
||||||
|
useClipboard()
|
||||||
|
.copy(deepLink)
|
||||||
|
.then(() => {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "toast",
|
||||||
|
title: "Copied",
|
||||||
|
text: "A link to this project was copied to the clipboard.",
|
||||||
|
},
|
||||||
|
2000,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Isn't there a better way to make this available to the template?
|
// Isn't there a better way to make this available to the template?
|
||||||
expandText() {
|
expandText() {
|
||||||
this.expanded = true;
|
this.expanded = true;
|
||||||
@@ -1299,7 +1337,7 @@ export default class ProjectViewView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// return an HTTPS URL if it's not a global URL
|
// return an HTTPS URL if it's not a global URL
|
||||||
addScheme(url: string) {
|
ensureScheme(url: string) {
|
||||||
if (!libsUtil.isGlobalUri(url)) {
|
if (!libsUtil.isGlobalUri(url)) {
|
||||||
return "https://" + url;
|
return "https://" + url;
|
||||||
}
|
}
|
||||||
@@ -1428,7 +1466,7 @@ export default class ProjectViewView extends Vue {
|
|||||||
this.apiServer,
|
this.apiServer,
|
||||||
this.axios,
|
this.axios,
|
||||||
);
|
);
|
||||||
if (result.type === "success") {
|
if (result.success) {
|
||||||
this.$notify(
|
this.$notify(
|
||||||
{
|
{
|
||||||
group: "alert",
|
group: "alert",
|
||||||
@@ -1460,7 +1498,13 @@ export default class ProjectViewView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
openHiddenDidDialog() {
|
openHiddenDidDialog() {
|
||||||
|
const shortestProjectId = this.projectId.startsWith(
|
||||||
|
serverUtil.ENDORSER_CH_HANDLE_PREFIX,
|
||||||
|
)
|
||||||
|
? this.projectId.substring(serverUtil.ENDORSER_CH_HANDLE_PREFIX.length)
|
||||||
|
: this.projectId;
|
||||||
(this.$refs.hiddenDidDialog as HiddenDidDialog).open(
|
(this.$refs.hiddenDidDialog as HiddenDidDialog).open(
|
||||||
|
"project/" + shortestProjectId,
|
||||||
"creator",
|
"creator",
|
||||||
this.issuerVisibleToDids,
|
this.issuerVisibleToDids,
|
||||||
this.allContacts,
|
this.allContacts,
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ import { Contact } from "../db/tables/contacts";
|
|||||||
import {
|
import {
|
||||||
GenericCredWrapper,
|
GenericCredWrapper,
|
||||||
GenericVerifiableCredential,
|
GenericVerifiableCredential,
|
||||||
ErrorResult,
|
CreateAndSubmitClaimResult,
|
||||||
} from "../interfaces";
|
} from "../interfaces";
|
||||||
import {
|
import {
|
||||||
BVC_MEETUPS_PROJECT_CLAIM_ID,
|
BVC_MEETUPS_PROJECT_CLAIM_ID,
|
||||||
@@ -230,7 +230,9 @@ export default class QuickActionBvcBeginView extends Vue {
|
|||||||
suppressMilliseconds: true,
|
suppressMilliseconds: true,
|
||||||
}) || "";
|
}) || "";
|
||||||
|
|
||||||
this.allMyDids = (await retrieveAllAccountsMetadata()).map((account) => account.did);
|
this.allMyDids = (await retrieveAllAccountsMetadata()).map(
|
||||||
|
(account) => account.did,
|
||||||
|
);
|
||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
const accountsDB = await accountsDBPromise;
|
const accountsDB = await accountsDBPromise;
|
||||||
await accountsDB.open();
|
await accountsDB.open();
|
||||||
@@ -296,28 +298,29 @@ export default class QuickActionBvcBeginView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// in parallel, make a confirmation for each selected claim and send them all to the server
|
// in parallel, make a confirmation for each selected claim and send them all to the server
|
||||||
const confirmResults = await Promise.allSettled(
|
const confirmResults: PromiseSettledResult<CreateAndSubmitClaimResult>[] =
|
||||||
this.claimsToConfirmSelected.map(async (jwtId) => {
|
await Promise.allSettled(
|
||||||
const record = this.claimsToConfirm.find(
|
this.claimsToConfirmSelected.map(async (jwtId) => {
|
||||||
(claim) => claim.id === jwtId,
|
const record = this.claimsToConfirm.find(
|
||||||
);
|
(claim) => claim.id === jwtId,
|
||||||
if (!record) {
|
);
|
||||||
return { type: "error", error: "Record not found." };
|
if (!record) {
|
||||||
}
|
return { success: false, error: "Record not found." };
|
||||||
return createAndSubmitConfirmation(
|
}
|
||||||
this.activeDid,
|
return createAndSubmitConfirmation(
|
||||||
record.claim as GenericVerifiableCredential,
|
this.activeDid,
|
||||||
record.id,
|
record.claim as GenericVerifiableCredential,
|
||||||
record.handleId,
|
record.id,
|
||||||
this.apiServer,
|
record.handleId,
|
||||||
axios,
|
this.apiServer,
|
||||||
);
|
axios,
|
||||||
}),
|
);
|
||||||
);
|
}),
|
||||||
|
);
|
||||||
// check for any rejected confirmations
|
// check for any rejected confirmations
|
||||||
const confirmsSucceeded = confirmResults.filter(
|
const confirmsSucceeded = confirmResults.filter(
|
||||||
(result) =>
|
// 'fulfilled' is the status in a successful PromiseFulfilledResult
|
||||||
result.status === "fulfilled" && result.value.type === "success",
|
(result) => result.status === "fulfilled" && result.value.success,
|
||||||
);
|
);
|
||||||
if (confirmsSucceeded.length < this.claimsToConfirmSelected.length) {
|
if (confirmsSucceeded.length < this.claimsToConfirmSelected.length) {
|
||||||
logger.error("Error sending confirmations:", confirmResults);
|
logger.error("Error sending confirmations:", confirmResults);
|
||||||
@@ -351,7 +354,7 @@ export default class QuickActionBvcBeginView extends Vue {
|
|||||||
undefined,
|
undefined,
|
||||||
BVC_MEETUPS_PROJECT_CLAIM_ID,
|
BVC_MEETUPS_PROJECT_CLAIM_ID,
|
||||||
);
|
);
|
||||||
giveSucceeded = giveResult.type === "success";
|
giveSucceeded = giveResult.success;
|
||||||
if (!giveSucceeded) {
|
if (!giveSucceeded) {
|
||||||
logger.error("Error sending give:", giveResult);
|
logger.error("Error sending give:", giveResult);
|
||||||
this.$notify(
|
this.$notify(
|
||||||
@@ -360,7 +363,7 @@ export default class QuickActionBvcBeginView extends Vue {
|
|||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text:
|
text:
|
||||||
(giveResult as ErrorResult)?.error?.userMessage ||
|
(giveResult as CreateAndSubmitClaimResult)?.error ||
|
||||||
"There was an error sending that give.",
|
"There was an error sending that give.",
|
||||||
},
|
},
|
||||||
5000,
|
5000,
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ export default class SearchAreaView extends Vue {
|
|||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
await db.open();
|
await db.open();
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
searchBoxes: [newSearchBox],
|
searchBoxes: searchBoxes as unknown, // Type assertion for Dexie compatibility
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.searchBox = newSearchBox;
|
this.searchBox = newSearchBox;
|
||||||
@@ -269,7 +269,7 @@ export default class SearchAreaView extends Vue {
|
|||||||
if (USE_DEXIE_DB) {
|
if (USE_DEXIE_DB) {
|
||||||
await db.open();
|
await db.open();
|
||||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||||
searchBoxes: [],
|
searchBoxes: "[]" as unknown as string, // Type assertion for Dexie compatibility
|
||||||
filterFeedByNearby: false,
|
filterFeedByNearby: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ export default class ShareMyContactInfoView extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "info",
|
type: "info",
|
||||||
title: "Copied",
|
title: "Copied",
|
||||||
text: "Your contact info was copied to the clipboard. Have them paste it in the box on their 'Contacts' screen.",
|
text: "Your contact info was copied to the clipboard. Have them click on it, or paste it in the box on their 'Contacts' screen.",
|
||||||
},
|
},
|
||||||
5000,
|
5000,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
</button>
|
</button>
|
||||||
Individual Profile
|
Individual Profile
|
||||||
</h1>
|
</h1>
|
||||||
|
<div class="text-sm text-center text-slate-500"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading Animation -->
|
<!-- Loading Animation -->
|
||||||
@@ -32,6 +33,12 @@
|
|||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<font-awesome icon="user" class="fa-fw text-slate-400"></font-awesome>
|
<font-awesome icon="user" class="fa-fw text-slate-400"></font-awesome>
|
||||||
{{ didInfo(profile.issuerDid, activeDid, allMyDids, allContacts) }}
|
{{ didInfo(profile.issuerDid, activeDid, allMyDids, allContacts) }}
|
||||||
|
<button title="Copy Link to Profile" @click="onCopyLinkClick()">
|
||||||
|
<font-awesome
|
||||||
|
icon="link"
|
||||||
|
class="text-sm text-slate-500 ml-2 mb-1"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="profile.description" class="mt-4 text-slate-600">
|
<p v-if="profile.description" class="mt-4 text-slate-600">
|
||||||
{{ profile.description }}
|
{{ profile.description }}
|
||||||
@@ -100,6 +107,7 @@ import { Router, RouteLocationNormalizedLoaded } from "vue-router";
|
|||||||
import QuickNav from "../components/QuickNav.vue";
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
import TopMessage from "../components/TopMessage.vue";
|
import TopMessage from "../components/TopMessage.vue";
|
||||||
import {
|
import {
|
||||||
|
APP_SERVER,
|
||||||
DEFAULT_PARTNER_API_SERVER,
|
DEFAULT_PARTNER_API_SERVER,
|
||||||
NotificationIface,
|
NotificationIface,
|
||||||
USE_DEXIE_DB,
|
USE_DEXIE_DB,
|
||||||
@@ -113,6 +121,7 @@ import { retrieveAccountDids } from "../libs/util";
|
|||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||||
import { Settings } from "@/db/tables/settings";
|
import { Settings } from "@/db/tables/settings";
|
||||||
|
import { useClipboard } from "@vueuse/core";
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
LMap,
|
LMap,
|
||||||
@@ -186,6 +195,10 @@ export default class UserProfileView extends Vue {
|
|||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
this.profile = result.data;
|
this.profile = result.data;
|
||||||
|
if (this.profile && this.profile.rowId !== profileId) {
|
||||||
|
// currently the server returns "rowid" with lowercase "i"; remove when that's fixed
|
||||||
|
this.profile.rowId = profileId;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Failed to load profile");
|
throw new Error("Failed to load profile");
|
||||||
}
|
}
|
||||||
@@ -204,5 +217,22 @@ export default class UserProfileView extends Vue {
|
|||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onCopyLinkClick() {
|
||||||
|
const deepLink = `${APP_SERVER}/deep-link/user-profile/${this.profile?.rowId}`;
|
||||||
|
useClipboard()
|
||||||
|
.copy(deepLink)
|
||||||
|
.then(() => {
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "toast",
|
||||||
|
title: "Copied",
|
||||||
|
text: "A link to this profile was copied to the clipboard.",
|
||||||
|
},
|
||||||
|
2000,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user