Compare commits

..

1 Commits

Author SHA1 Message Date
Matthew Raymer
3881144c62 feat: add missing settings management functions
- Add getSettingsForAccount() to retrieve complete settings for any account DID
- Add getAccountSpecificSettings() to get account-specific settings without defaults
- Add mergeSettings() for consistent settings merging across SQLite and Dexie
- Update retrieveSettingsForActiveAccount() to use new mergeSettings() function
- Improve error handling and logging for settings operations
- Maintain backward compatibility with existing settings API

This provides better settings management capabilities for account switching,
debugging, and data validation while ensuring consistent behavior across
both SQLite and Dexie storage implementations.
2025-06-10 12:44:52 +00:00
54 changed files with 1319 additions and 1955 deletions

1
.gitignore vendored
View File

@@ -55,4 +55,3 @@ build_logs/
icons icons
android/app/src/main/res/

View File

@@ -41,7 +41,6 @@ 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
``` ```
@@ -65,8 +64,6 @@ 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`.
@@ -74,7 +71,7 @@ Install dependencies:
* For test, build the app (because test server is not yet set up to build): * For test, build the app (because test server is not yet set up to build):
```bash ```bash
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_DEFAULT_PUSH_SERVER=https://test.timesafari.app VITE_PASSKEYS_ENABLED=true npm run build:web TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_DEFAULT_PUSH_SERVER=https://test.timesafari.app VITE_PASSKEYS_ENABLED=true npm run build
``` ```
... and transfer to the test server: ... and transfer to the test server:
@@ -324,11 +321,11 @@ Prerequisites: macOS with Xcode installed
#### Each Release #### Each Release
0. First time (or if dependencies change): 0. First time (or if XCode dependencies change):
- `pkgx +rubygems.org sh` - `pkgx +rubygems.org sh`
- ... and you may have to fix these, especially with pkgx: - ... and you may have to fix these, especially with pkgx
```bash ```bash
gem_path=$(which gem) gem_path=$(which gem)
@@ -337,12 +334,23 @@ Prerequisites: macOS with Xcode installed
export GEM_PATH=$shortened_path export GEM_PATH=$shortened_path
``` ```
1. Build the web assets & update ios: ```bash
cd ios/App
pod install
```
1. Build the web assets:
```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
``` ```
@@ -359,14 +367,15 @@ Prerequisites: macOS with Xcode installed
npx capacitor-assets generate --ios npx capacitor-assets generate --ios
``` ```
4. Bump the version to match Android & package.json: 4. Bump the version to match Android:
``` ```
cd ios/App cd ios/App
xcrun agvtool new-version 34 xcrun agvtool new-version 25
# Unfortunately this edits Info.plist directly. # Unfortunately this edits Info.plist directly.
#xcrun agvtool new-marketing-version 0.4.5 #xcrun agvtool new-marketing-version 0.4.5
cat App.xcodeproj/project.pbxproj | sed "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 0.5.8;/g" > temp && mv temp App.xcodeproj/project.pbxproj cat App.xcodeproj/project.pbxproj | sed "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 0.5.0;/g" > temp
mv temp App.xcodeproj/project.pbxproj
cd - cd -
``` ```
@@ -418,7 +427,7 @@ Prerequisites: Android Studio with Java SDK installed
npx capacitor-assets generate --android npx capacitor-assets generate --android
``` ```
4. Bump version to match iOS & package.json: android/app/build.gradle 4. Bump version to match iOS: android/app/build.gradle
5. Open the project in Android Studio: 5. Open the project in Android Studio:
@@ -435,6 +444,7 @@ Prerequisites: Android Studio with Java 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`:
@@ -468,7 +478,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.
## Android Configuration for deep links ## First-time 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,6 +489,4 @@ 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]

View File

@@ -6,13 +6,6 @@ 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

View File

@@ -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 34 versionCode 25
versionName "0.5.8" versionName "0.5.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions { aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.

View File

@@ -100,7 +100,6 @@ 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

View File

@@ -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 = 34; CURRENT_PROJECT_VERSION = 25;
DEVELOPMENT_TEAM = GM3FS5JQPH; DEVELOPMENT_TEAM = GM3FS5JQPH;
ENABLE_APP_SANDBOX = NO; ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -413,7 +413,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.5.8; MARKETING_VERSION = 0.5.0;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari; PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@@ -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 = 34; CURRENT_PROJECT_VERSION = 25;
DEVELOPMENT_TEAM = GM3FS5JQPH; DEVELOPMENT_TEAM = GM3FS5JQPH;
ENABLE_APP_SANDBOX = NO; ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -440,7 +440,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 0.5.8; MARKETING_VERSION = 0.5.0;
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari; PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";

View File

@@ -49,16 +49,5 @@
</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>

1215
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "timesafari", "name": "timesafari",
"version": "0.5.8", "version": "0.4.8",
"description": "Time Safari Application", "description": "Time Safari Application",
"author": { "author": {
"name": "Time Safari Team" "name": "Time Safari Team"

View File

@@ -104,6 +104,7 @@ 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: {
@@ -142,23 +143,19 @@ 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 databaseUtil.updateDefaultSettings({ await db.settings.update(MASTER_SETTINGS_KEY, {
filterFeedByVisible: this.hasVisibleDid, filterFeedByVisible: this.hasVisibleDid,
}); });
if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, {
filterFeedByVisible: this.hasVisibleDid,
});
}
} }
async toggleNearby() { async toggleNearby() {
this.settingChanged = true; this.settingChanged = true;
this.isNearby = !this.isNearby; this.isNearby = !this.isNearby;
await databaseUtil.updateDefaultSettings({ const platformService = PlatformServiceFactory.getInstance();
filterFeedByNearby: this.isNearby, await platformService.dbExec(
}); `UPDATE settings SET filterFeedByNearby = ? WHERE id = ?`,
[this.isNearby, MASTER_SETTINGS_KEY],
);
if (USE_DEXIE_DB) { if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, { await db.settings.update(MASTER_SETTINGS_KEY, {
@@ -172,10 +169,11 @@ export default class FeedFilters extends Vue {
this.settingChanged = true; this.settingChanged = true;
} }
await databaseUtil.updateDefaultSettings({ const platformService = PlatformServiceFactory.getInstance();
filterFeedByNearby: false, await platformService.dbExec(
filterFeedByVisible: false, `UPDATE settings SET filterFeedByNearby = ? AND filterFeedByVisible = ? WHERE id = ?`,
}); [false, false, MASTER_SETTINGS_KEY],
);
if (USE_DEXIE_DB) { if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, { await db.settings.update(MASTER_SETTINGS_KEY, {
@@ -193,10 +191,11 @@ export default class FeedFilters extends Vue {
this.settingChanged = true; this.settingChanged = true;
} }
await databaseUtil.updateDefaultSettings({ const platformService = PlatformServiceFactory.getInstance();
filterFeedByNearby: true, await platformService.dbExec(
filterFeedByVisible: true, `UPDATE settings SET filterFeedByNearby = ? AND filterFeedByVisible = ? WHERE id = ?`,
}); [true, true, MASTER_SETTINGS_KEY],
);
if (USE_DEXIE_DB) { if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, { await db.settings.update(MASTER_SETTINGS_KEY, {

View File

@@ -321,7 +321,7 @@ export default class GiftedDialog extends Vue {
); );
if (!result.success) { if (!result.success) {
const errorMessage = result.error; 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(
{ {
@@ -367,6 +367,19 @@ 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(
{ {

View File

@@ -227,7 +227,6 @@ 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 &&
@@ -246,8 +245,9 @@ export default class GivenPrompts extends Vue {
[someContactDbIndex], [someContactDbIndex],
); );
if (result) { if (result) {
const mappedContacts = databaseUtil.mapQueryResultToValues(result); this.currentContact = databaseUtil.mapQueryResultToValues(result)[
this.currentContact = mappedContacts[0] as unknown as Contact; someContactDbIndex
] as unknown as Contact;
} }
if (USE_DEXIE_DB) { if (USE_DEXIE_DB) {
await db.open(); await db.open();

View File

@@ -48,15 +48,12 @@
<span> <span>
{{ didInfo(visDid) }} {{ didInfo(visDid) }}
<span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)"> <span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)">
<router-link <a :href="`/did/${visDid}`" class="text-blue-500">
:to="{ path: '/did/' + encodeURIComponent(visDid) }"
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"
/> />
</router-link> </a>
</span> </span>
</span> </span>
</div> </div>
@@ -77,7 +74,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', deepLinkUrl)" @click="copyToClipboard('A link to this page', windowLocation)"
>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 +101,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 { APP_SERVER, NotificationIface } from "../constants/app"; import { NotificationIface } from "../constants/app";
@Component @Component
export default class HiddenDidDialog extends Vue { export default class HiddenDidDialog extends Vue {
@@ -117,8 +114,7 @@ export default class HiddenDidDialog extends Vue {
activeDid = ""; activeDid = "";
allMyDids: Array<string> = []; allMyDids: Array<string> = [];
canShare = false; canShare = false;
deepLinkPathSuffix = ""; windowLocation = window.location.href;
deepLinkUrl = window.location.href; // this is changed to a deep link in the setup
R = R; R = R;
serverUtil = serverUtil; serverUtil = serverUtil;
@@ -130,21 +126,17 @@ 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;
} }
@@ -178,11 +170,11 @@ export default class HiddenDidDialog extends Vue {
} }
onClickShareClaim() { onClickShareClaim() {
this.copyToClipboard("A link to this page", this.deepLinkUrl); this.copyToClipboard("A link to this page", this.windowLocation);
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.deepLinkUrl, url: this.windowLocation,
}); });
} }
} }

View File

@@ -83,7 +83,10 @@
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 { createAndSubmitOffer } from "../libs/endorserServer"; import {
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";
@@ -247,7 +250,7 @@ export default class OfferDialog extends Vue {
); );
if (!result.success) { if (!result.success) {
const errorMessage = result.error; const errorMessage = this.getOfferCreationErrorMessage(result);
logger.error("Error with offer creation result:", result); logger.error("Error with offer creation result:", result);
this.$notify( this.$notify(
{ {
@@ -287,6 +290,21 @@ 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>

View File

@@ -259,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.updateDidSpecificSettings(this.activeDid, { await databaseUtil.updateAccountSettings(this.activeDid, {
finishedOnboarding: true, finishedOnboarding: true,
}); });
if (USE_DEXIE_DB) { if (USE_DEXIE_DB) {
@@ -273,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.updateDidSpecificSettings(this.activeDid, { await databaseUtil.updateAccountSettings(this.activeDid, {
finishedOnboarding: true, finishedOnboarding: true,
}); });
if (USE_DEXIE_DB) { if (USE_DEXIE_DB) {

View File

@@ -38,13 +38,14 @@ 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 not using prod, user " + didPrefix; this.message = "You're linked to a non-prod server, 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 = "You are using prod, user " + didPrefix; this.message =
"You're linked to the production server, user " + didPrefix;
} }
} catch (err: unknown) { } catch (err: unknown) {
this.$notify( this.$notify(

View File

@@ -37,20 +37,7 @@ export async function updateDefaultSettings(
} }
} }
export async function insertDidSpecificSettings( export async function updateAccountSettings(
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> {
@@ -109,6 +96,9 @@ 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;
} }
@@ -121,29 +111,20 @@ export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
); );
if (!result?.values?.length) { if (!result?.values?.length) {
// we created DID-specific settings when generated or imported, so this shouldn't happen logConsoleAndDb(
`[databaseUtil] No account-specific settings found for ${defaultSettings.activeDid}`,
);
return defaultSettings; return defaultSettings;
} }
// Map and filter settings // Map settings
const overrideSettings = mapColumnsToValues( const overrideSettings = mapColumnsToValues(
result.columns, result.columns,
result.values, result.values,
)[0] as Settings; )[0] as Settings;
const overrideSettingsFiltered = Object.fromEntries( // Use the new mergeSettings function for consistency
Object.entries(overrideSettings).filter(([_, v]) => v !== null), return mergeSettings(defaultSettings, overrideSettings);
);
// Merge settings
const settings = { ...defaultSettings, ...overrideSettingsFiltered };
// Handle searchBoxes parsing
if (settings.searchBoxes) {
settings.searchBoxes = parseJsonField(settings.searchBoxes, []);
}
return settings;
} catch (error) { } catch (error) {
logConsoleAndDb( logConsoleAndDb(
`[databaseUtil] Failed to retrieve account settings for ${defaultSettings.activeDid}: ${error}`, `[databaseUtil] Failed to retrieve account settings for ${defaultSettings.activeDid}: ${error}`,
@@ -219,9 +200,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);
} }
@@ -240,7 +221,6 @@ 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,
@@ -314,113 +294,126 @@ export function mapColumnsToValues(
} }
/** /**
* Debug function to inspect raw settings data in the database * Retrieves settings for a specific account by DID
* This helps diagnose issues with data corruption or malformed JSON * @param accountDid - The DID of the account to retrieve settings for
* @param did Optional DID to inspect specific account settings * @returns Promise<Settings> Combined settings for the specified account
* @author Matthew Raymer
*/ */
export async function debugSettingsData(did?: string): Promise<void> { export async function getSettingsForAccount(accountDid: string): Promise<Settings> {
try { try {
// Get default settings first
const defaultSettings = await retrieveSettingsForDefaultAccount();
// Get account-specific settings
const platform = PlatformServiceFactory.getInstance(); const platform = PlatformServiceFactory.getInstance();
const result = await platform.dbQuery(
// Get all settings records "SELECT * FROM settings WHERE accountDid = ?",
const allSettings = await platform.dbQuery("SELECT * FROM settings"); [accountDid],
logConsoleAndDb(
`[DEBUG] Total settings records: ${allSettings?.values?.length || 0}`,
false,
); );
if (allSettings?.values?.length) { if (!result?.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( logConsoleAndDb(
`[DEBUG] Account for ${did}: ${JSON.stringify(account, null, 2)}`, `[databaseUtil] No account-specific settings found for ${accountDid}`,
false,
); );
return defaultSettings;
} }
// Map and merge settings
const overrideSettings = mapColumnsToValues(
result.columns,
result.values,
)[0] as Settings;
return mergeSettings(defaultSettings, overrideSettings);
} catch (error) { } catch (error) {
logConsoleAndDb(`[DEBUG] Error inspecting settings data: ${error}`, true); logConsoleAndDb(
`[databaseUtil] Failed to retrieve settings for account ${accountDid}: ${error}`,
true,
);
// Return default settings on error
return await retrieveSettingsForDefaultAccount();
} }
} }
/** /**
* Platform-agnostic JSON parsing utility * Retrieves only account-specific settings for a given DID (without merging with defaults)
* Handles different SQLite implementations: * @param accountDid - The DID of the account to retrieve settings for
* - Web SQLite (wa-sqlite/absurd-sql): Auto-parses JSON strings to objects * @returns Promise<Settings> Account-specific settings only
* - 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 { export async function getAccountSpecificSettings(accountDid: string): Promise<Settings> {
try { try {
// If already an object (web SQLite auto-parsed), return as-is const platform = PlatformServiceFactory.getInstance();
if (typeof value === "object" && value !== null) { const result = await platform.dbQuery(
return value as T; "SELECT * FROM settings WHERE accountDid = ?",
[accountDid],
);
if (!result?.values?.length) {
logConsoleAndDb(
`[databaseUtil] No account-specific settings found for ${accountDid}`,
);
return {};
} }
// If it's a string (Capacitor SQLite or fallback), parse it // Map settings
if (typeof value === "string") { const settings = mapColumnsToValues(
return JSON.parse(value) as T; result.columns,
result.values,
)[0] as Settings;
// Handle searchBoxes parsing
if (settings.searchBoxes) {
try {
// @ts-expect-error - the searchBoxes field is a string in the DB
settings.searchBoxes = JSON.parse(settings.searchBoxes);
} catch (error) {
logConsoleAndDb(
`[databaseUtil] Failed to parse searchBoxes for ${accountDid}: ${error}`,
true,
);
// Reset to empty array on parse failure
settings.searchBoxes = [];
}
} }
// If it's null/undefined, return default return settings;
if (value === null || value === undefined) {
return defaultValue;
}
return defaultValue;
} catch (error) { } catch (error) {
logConsoleAndDb( logConsoleAndDb(
`[databaseUtil] Failed to parse JSON field: ${error}`, `[databaseUtil] Failed to retrieve account-specific settings for ${accountDid}: ${error}`,
true, true,
); );
return defaultValue; return {};
} }
} }
/**
* Merges default settings with account-specific settings using consistent logic
* @param defaultSettings - The default/master settings
* @param accountSettings - The account-specific settings to merge
* @returns Settings - Merged settings with account-specific overrides
*/
export function mergeSettings(defaultSettings: Settings, accountSettings: Settings): Settings {
// Filter out null values from account settings
const accountSettingsFiltered = Object.fromEntries(
Object.entries(accountSettings).filter(([_, v]) => v !== null),
);
// Perform shallow merge (account settings override defaults)
const mergedSettings = { ...defaultSettings, ...accountSettingsFiltered };
// Handle searchBoxes parsing if present
if (mergedSettings.searchBoxes) {
try {
// @ts-expect-error - the searchBoxes field is a string in the DB
mergedSettings.searchBoxes = JSON.parse(mergedSettings.searchBoxes);
} catch (error) {
logConsoleAndDb(
`[databaseUtil] Failed to parse searchBoxes during merge: ${error}`,
true,
);
// Reset to empty array on parse failure
mergedSettings.searchBoxes = [];
}
}
return mergedSettings;
}

View File

@@ -265,6 +265,43 @@ export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
} }
} }
/**
* Retrieves settings for a specific account by DID
* @param accountDid - The DID of the account to retrieve settings for
* @returns Promise<Settings> Combined settings for the specified account
*/
export async function getSettingsForAccount(accountDid: string): Promise<Settings> {
const defaultSettings = await retrieveSettingsForDefaultAccount();
const overrideSettings =
(await db.settings
.where("accountDid")
.equals(accountDid)
.first()) || {};
return R.mergeDeepRight(defaultSettings, overrideSettings);
}
/**
* Retrieves only account-specific settings for a given DID (without merging with defaults)
* @param accountDid - The DID of the account to retrieve settings for
* @returns Promise<Settings> Account-specific settings only
*/
export async function getAccountSpecificSettings(accountDid: string): Promise<Settings> {
return (await db.settings
.where("accountDid")
.equals(accountDid)
.first()) || {};
}
/**
* Merges default settings with account-specific settings using consistent logic
* @param defaultSettings - The default/master settings
* @param accountSettings - The account-specific settings to merge
* @returns Settings - Merged settings with account-specific overrides
*/
export function mergeSettings(defaultSettings: Settings, accountSettings: Settings): Settings {
return R.mergeDeepRight(defaultSettings, accountSettings);
}
export async function updateAccountSettings( export async function updateAccountSettings(
accountDid: string, accountDid: string,
settingsChanges: Settings, settingsChanges: Settings,

View File

@@ -1,4 +1,6 @@
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;
@@ -45,3 +47,12 @@ 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>;
}

View File

@@ -15,6 +15,10 @@ 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;
@@ -26,6 +30,11 @@ 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;

View File

@@ -29,17 +29,18 @@ 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 = [
// note that similar lists are below in deepLinkSchemas and in src/services/deepLinks.ts
"claim",
"claim-add-raw",
"claim-cert",
"confirm-gift",
"contact-import",
"did",
"invite-one-accept",
"onboard-meeting-setup",
"project",
"user-profile", "user-profile",
"project-details",
"onboard-meeting-setup",
"invite-one-accept",
"contact-import",
"confirm-gift",
"claim",
"claim-cert",
"claim-add-raw",
"contact-edit",
"contacts",
"did",
] as const; ] as const;
// Create a type from the array // Create a type from the array
@@ -57,39 +58,44 @@ 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 = {
// note that similar lists are above in VALID_DEEP_LINK_ROUTES and in src/services/deepLinks.ts "user-profile": z.object({
id: z.string(),
}),
"project-details": z.object({
id: z.string(),
}),
"onboard-meeting-setup": z.object({
id: z.string(),
}),
"invite-one-accept": z.object({
id: z.string(),
}),
"contact-import": z.object({
jwt: z.string(),
}),
"confirm-gift": z.object({
id: z.string(),
}),
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(),
}), }),
"claim-cert": z.object({ "contact-edit": z.object({
id: z.string(), did: z.string(),
}), }),
"confirm-gift": z.object({ contacts: z.object({
id: z.string(), contacts: z.string(), // JSON string of contacts array
}),
"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 = {

View File

@@ -1,6 +1,5 @@
export type { export type {
// From common.ts // From common.ts
CreateAndSubmitClaimResult,
GenericCredWrapper, GenericCredWrapper,
GenericVerifiableCredential, GenericVerifiableCredential,
KeyMeta, KeyMeta,
@@ -19,6 +18,11 @@ 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,

View File

@@ -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,8 +1074,7 @@ export async function generateEndorserJwtUrlForAccount(
const vcJwt = await createEndorserJwtForDid(account.did, contactInfo); const vcJwt = await createEndorserJwtForDid(account.did, contactInfo);
const viewPrefix = const viewPrefix = APP_SERVER + CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI;
APP_SERVER + "/deep-link" + CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI;
return viewPrefix + vcJwt; return viewPrefix + vcJwt;
} }

View File

@@ -44,7 +44,6 @@ 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;
@@ -698,7 +697,6 @@ 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
@@ -712,7 +710,6 @@ 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);
@@ -735,9 +732,7 @@ 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.updateDidSpecificSettings(newId.did, { await databaseUtil.updateAccountSettings(newId.did, { isRegistered: false });
isRegistered: false,
});
if (USE_DEXIE_DB) { if (USE_DEXIE_DB) {
await updateAccountSettings(newId.did, { isRegistered: false }); await updateAccountSettings(newId.did, { isRegistered: false });
} }
@@ -779,7 +774,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.updateDidSpecificSettings(account.did, { await databaseUtil.updateAccountSettings(account.did, {
isRegistered: false, isRegistered: false,
}); });
if (USE_DEXIE_DB) { if (USE_DEXIE_DB) {
@@ -867,7 +862,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(parseJsonField(contact.contactMethods, []))) ? escapeField(JSON.stringify(contact.contactMethods))
: ""; : "";
const fields = [ const fields = [
@@ -882,71 +877,6 @@ 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
*/ */
@@ -977,7 +907,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(parseJsonField(contact.contactMethods, [])) ? JSON.stringify(contact.contactMethods)
: null, : null,
nextPubKeyHashB64: contact.nextPubKeyHashB64 || null, nextPubKeyHashB64: contact.nextPubKeyHashB64 || null,
notes: contact.notes || null, notes: contact.notes || null,

View File

@@ -34,7 +34,8 @@ 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 { logger, safeStringify } from "./utils/logger"; import { logConsoleAndDb } from "./db/databaseUtil";
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);
@@ -71,10 +72,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) {
logger.error("[DeepLink] Error handling deep link: ", error); logConsoleAndDb("[DeepLink] Error handling deep link: " + error, true);
handleApiError( handleApiError(
{ {
message: error instanceof Error ? error.message : safeStringify(error), message: error instanceof Error ? error.message : String(error),
} as AxiosError, } as AxiosError,
"deep-link", "deep-link",
); );

View File

@@ -83,11 +83,6 @@ 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",

View File

@@ -6,7 +6,7 @@
*/ */
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import { logger, safeStringify } from "../utils/logger"; import { logger } from "../utils/logger";
/** /**
* Handles API errors with platform-specific logging and error processing. * Handles API errors with platform-specific logging and error processing.
@@ -37,8 +37,7 @@ import { logger, safeStringify } 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") {
const endpointStr = safeStringify(endpoint); // we've seen this as an object in deep links logger.error(`[Capacitor API Error] ${endpoint}:`, {
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,

View File

@@ -27,16 +27,18 @@
* timesafari://<route>[/<param>][?queryParam1=value1&queryParam2=value2] * timesafari://<route>[/<param>][?queryParam1=value1&queryParam2=value2]
* *
* Supported Routes: * Supported Routes:
* - claim: View claim
* - claim-add-raw: Add raw claim
* - claim-cert: View claim certificate
* - confirm-gift
* - contact-import: Import contacts
* - did: View DID
* - invite-one-accept: Accept invitation
* - onboard-meeting-members
* - project: View project details
* - user-profile: View user profile * - 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-cert: View claim certificate
* - claim-add-raw: Add raw claim
* - contact-edit: Edit contact
* - contacts: View contacts
* - did: View DID
* *
* @example * @example
* const handler = new DeepLinkHandler(router); * const handler = new DeepLinkHandler(router);
@@ -79,17 +81,18 @@ export class DeepLinkHandler {
string, string,
{ name: string; paramKey?: string } { name: string; paramKey?: string }
> = { > = {
// note that similar lists are in src/interfaces/deepLinks.ts
claim: { name: "claim" },
"claim-add-raw": { name: "claim-add-raw" },
"claim-cert": { name: "claim-cert" },
"confirm-gift": { name: "confirm-gift" },
"contact-import": { name: "contact-import", paramKey: "jwt" },
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" }, "user-profile": { name: "user-profile" },
"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-cert": { name: "claim-cert" },
"claim-add-raw": { name: "claim-add-raw" },
"contact-edit": { name: "contact-edit", paramKey: "did" },
contacts: { name: "contacts" },
did: { name: "did", paramKey: "did" },
}; };
/** /**
@@ -98,7 +101,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: string, params: {KEY: string}, query: {KEY: string}) * @returns Parsed URL components (path, params, query)
*/ */
private parseDeepLink(url: string) { private parseDeepLink(url: string) {
const parts = url.split("://"); const parts = url.split("://");
@@ -114,16 +117,7 @@ export class DeepLinkHandler {
}); });
const [path, queryString] = parts[1].split("?"); const [path, queryString] = parts[1].split("?");
const [routePath, ...pathParams] = path.split("/"); const [routePath, param] = 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]) {
@@ -142,14 +136,45 @@ export class DeepLinkHandler {
} }
const params: Record<string, string> = {}; const params: Record<string, string> = {};
if (pathParams) { if (param) {
// 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"] = pathParams.join("/"); params[routeConfig.paramKey ?? "id"] = param;
} }
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.
@@ -220,39 +245,6 @@ 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,
}; };
} }
} }

View File

@@ -5,7 +5,6 @@ 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,
@@ -248,7 +247,7 @@ export class CapacitorPlatformService implements PlatformService {
hasFileSystem: true, hasFileSystem: true,
hasCamera: true, hasCamera: true,
isMobile: true, isMobile: true,
isIOS: Capacitor.getPlatform() === "ios", isIOS: /iPad|iPhone|iPod/.test(navigator.userAgent),
hasFileDownload: false, hasFileDownload: false,
needsFileHandlingInstructions: true, needsFileHandlingInstructions: true,
isNativeApp: true, isNativeApp: true,

View File

@@ -1,6 +1,6 @@
import { logToDb } from "../db/databaseUtil"; import { logToDb } from "../db/databaseUtil";
export function safeStringify(obj: unknown) { 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,9 +67,8 @@ 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 messageString = safeStringify(message); const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
const argsString = args.length > 0 ? safeStringify(args) : ""; logToDb(message + argsString);
logToDb(messageString + argsString);
}, },
}; };

View File

@@ -1814,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.updateDidSpecificSettings(did, { await databaseUtil.updateAccountSettings(did, {
isRegistered: true, isRegistered: true,
}); });
if (USE_DEXIE_DB) { if (USE_DEXIE_DB) {
@@ -2018,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.updateDidSpecificSettings(this.activeDid, { await databaseUtil.updateAccountSettings(this.activeDid, {
profileImageUrl: undefined, profileImageUrl: undefined,
}); });
if (USE_DEXIE_DB) { if (USE_DEXIE_DB) {

View File

@@ -198,7 +198,7 @@ export default class ClaimAddRawView extends Vue {
this.apiServer, this.apiServer,
this.axios, this.axios,
); );
if (result.success) { if (result.type === "success") {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",

View File

@@ -46,35 +46,23 @@
</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="View Printable Certificate" title="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="copyToClipboard('A link to this page', windowDeepLink)" @click="
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>
@@ -304,17 +292,12 @@
<div class="text-sm"> <div class="text-sm">
{{ didInfo(confirmerId) }} {{ didInfo(confirmerId) }}
<span v-if="!serverUtil.isEmptyOrHiddenDid(confirmerId)"> <span v-if="!serverUtil.isEmptyOrHiddenDid(confirmerId)">
<router-link <a :href="`/did/${confirmerId}`" class="text-blue-500">
:to="{
path: '/did/' + encodeURIComponent(confirmerId),
}"
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"
/> />
</router-link> </a>
</span> </span>
</div> </div>
</div> </div>
@@ -346,17 +329,12 @@
<div class="text-sm"> <div class="text-sm">
{{ didInfo(confsVisibleTo) }} {{ didInfo(confsVisibleTo) }}
<span v-if="!serverUtil.isEmptyOrHiddenDid(confsVisibleTo)"> <span v-if="!serverUtil.isEmptyOrHiddenDid(confsVisibleTo)">
<router-link <a :href="`/did/${confsVisibleTo}`" class="text-blue-500">
:to="{
path: '/did/' + encodeURIComponent(confsVisibleTo),
}"
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"
/> />
</router-link> </a>
</span> </span>
</div> </div>
</div> </div>
@@ -416,7 +394,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', windowDeepLink)" @click="copyToClipboard('A link to this page', windowLocation)"
>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
@@ -439,7 +417,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', windowDeepLink)" @click="copyToClipboard('A link to this page', windowLocation)"
>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
> >
@@ -465,17 +443,12 @@
<span> <span>
{{ didInfo(visDid) }} {{ didInfo(visDid) }}
<span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)"> <span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)">
<router-link <a :href="`/did/${visDid}`" class="text-blue-500">
:to="{
path: '/did/' + encodeURIComponent(visDid),
}"
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"
/> />
</router-link> </a>
</span> </span>
<span v-if="veriClaim.publicUrls?.[visDid]" <span v-if="veriClaim.publicUrls?.[visDid]"
>, found at&nbsp;<a >, found at&nbsp;<a
@@ -557,7 +530,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 { APP_SERVER, NotificationIface, USE_DEXIE_DB } from "../constants/app"; import { 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";
@@ -604,9 +577,8 @@ 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[] } = {};
windowDeepLink = window.location.href; // changed in the setup for deep linking windowLocation = window.location.href;
APP_SERVER = APP_SERVER;
R = R; R = R;
yaml = yaml; yaml = yaml;
libsUtil = libsUtil; libsUtil = libsUtil;
@@ -683,7 +655,6 @@ export default class ClaimView extends Vue {
5000, 5000,
); );
} }
this.windowDeepLink = `${APP_SERVER}/deep-link/claim/${claimId}`;
this.canShare = !!navigator.share; this.canShare = !!navigator.share;
} }
@@ -954,7 +925,7 @@ export default class ClaimView extends Vue {
this.apiServer, this.apiServer,
this.axios, this.axios,
); );
if (result.success) { if (result.type === "success") {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -1019,11 +990,11 @@ export default class ClaimView extends Vue {
} }
onClickShareClaim() { onClickShareClaim() {
this.copyToClipboard("A link to this page", this.windowDeepLink); this.copyToClipboard("A link to this page", this.windowLocation);
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.windowDeepLink, url: this.windowLocation,
}); });
} }

View File

@@ -407,14 +407,14 @@
</a> </a>
</div> </div>
<div class="mt-2 ml-2"> <div class="mt-2 ml-2">
<router-link <a
v-if="isRegistered" v-if="isRegistered"
class="text-blue-500 cursor-pointer" class="text-blue-500 cursor-pointer"
:to="urlForNewGive" :href="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
</router-link> </a>
</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 { APP_SERVER, NotificationIface, USE_DEXIE_DB } from "../constants/app"; import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
import { db, retrieveSettingsForActiveAccount } from "../db/index"; import { db, retrieveSettingsForActiveAccount } from "../db/index";
import { 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; // this is changed to a deep link in the setup windowLocation = window.location.href;
R = R; R = R;
yaml = yaml; yaml = yaml;
@@ -566,9 +566,6 @@ 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);
} }
@@ -679,12 +676,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)}`;
} }
@@ -834,7 +831,7 @@ export default class ConfirmGiftView extends Vue {
this.apiServer, this.apiServer,
this.axios, this.axios,
); );
if (result.success) { if (result.type === "success") {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",

View File

@@ -138,13 +138,11 @@ 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 { NotificationIface, USE_DEXIE_DB } from "../constants/app"; import { AppString, NotificationIface, USE_DEXIE_DB } from "../constants/app";
import * as databaseUtil from "../db/databaseUtil";
import { parseJsonField } from "../db/databaseUtil";
import { db } from "../db/index"; import { db } from "../db/index";
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
import { Contact, ContactMethod } from "../db/tables/contacts"; import { Contact, ContactMethod } from "../db/tables/contacts";
import { AppString } from "../constants/app"; import * as databaseUtil from "../db/databaseUtil";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
/** /**
* Contact Edit View Component * Contact Edit View Component
@@ -232,7 +230,9 @@ 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 = parseJsonField(contact?.contactMethods, []); contact.contactMethods = JSON.parse(
(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 || "");

View File

@@ -213,7 +213,6 @@ 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,
@@ -290,7 +289,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: parseJsonField(record.contactMethods, []), contactMethods: JSON.parse(record.contactMethods || "[]"),
}; };
} }

View File

@@ -104,7 +104,6 @@
</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";
@@ -118,20 +117,13 @@ 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 { import { setVisibilityUtil } from "../libs/endorserServer";
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;
@@ -149,7 +141,7 @@ interface IUserNameDialog {
UserNameDialog, UserNameDialog,
}, },
}) })
export default class ContactQRScanFull extends Vue { export default class ContactQRScan extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router; $router!: Router;
@@ -158,8 +150,6 @@ export default class ContactQRScanFull 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;
@@ -181,22 +171,19 @@ export default class ContactQRScanFull 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}` : "");
const publicKeyBase64 = Buffer.from( this.qrValue = await generateEndorserJwtUrlForAccount(
account.publicKeyHex, account,
"hex", !!settings.isRegistered,
).toString("base64"); name,
this.qrValue = settings.profileImageUrl || "",
CONTACT_CSV_HEADER + false,
"\n" + );
`"${name}",${account.did},${publicKeyBase64},false,${this.isRegistered}`;
} }
} catch (error) { } catch (error) {
logger.error("Error initializing component:", { logger.error("Error initializing component:", {
@@ -348,69 +335,57 @@ export default class ContactQRScanFull extends Vue {
logger.info("Processing QR code scan result:", rawValue); logger.info("Processing QR code scan result:", rawValue);
let contact: Contact; // Extract JWT
if (rawValue.includes(CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI)) { const jwt = getContactJwtFromJwtUrl(rawValue);
// Extract JWT 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;
}
// 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: "Error", title: "Invalid 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.", text: "This QR code does not contain valid contact information. Please scan a TimeSafari contact QR code.",
}); });
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,
@@ -492,16 +467,14 @@ export default class ContactQRScanFull 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.",
}, },
5000, 3000,
); );
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 = JSON.stringify(contact.contactMethods);
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",
@@ -592,19 +565,9 @@ export default class ContactQRScanFull extends Vue {
); );
} }
async onCopyUrlToClipboard() { 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(jwtUrl) .copy(this.qrValue)
.then(() => { .then(() => {
this.$notify( this.$notify(
{ {

View File

@@ -159,7 +159,6 @@
<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";
@@ -172,23 +171,19 @@ 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 * as libsUtil from "../libs/util"; import { retrieveAccountMetadata } 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;
@@ -218,7 +213,6 @@ 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
@@ -256,21 +250,19 @@ 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 libsUtil.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}` : "");
const publicKeyBase64 = Buffer.from( this.qrValue = await generateEndorserJwtUrlForAccount(
account.publicKeyHex, account,
"hex", !!settings.isRegistered,
).toString("base64"); name,
this.qrValue = settings.profileImageUrl || "",
CONTACT_CSV_HEADER + false,
"\n" + );
`"${name}",${account.did},${publicKeyBase64},false,${this.isRegistered}`;
} }
} catch (error) { } catch (error) {
logger.error("Error initializing component:", { logger.error("Error initializing component:", {
@@ -281,7 +273,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 renderer or scanner. Please try again.", text: "Failed to initialize QR scanner. Please try again.",
}); });
} }
} }
@@ -468,68 +460,53 @@ export default class ContactQRScanShow extends Vue {
logger.info("Processing QR code scan result:", rawValue); logger.info("Processing QR code scan result:", rawValue);
let contact: Contact; // Extract JWT
if (rawValue.includes(CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI)) { const jwt = getContactJwtFromJwtUrl(rawValue);
const jwt = getContactJwtFromJwtUrl(rawValue); if (!jwt) {
if (!jwt) { logger.warn("Invalid QR code format - no JWT found in URL");
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: "Error", title: "Invalid 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.", text: "This QR code does not contain valid contact information. Please scan a TimeSafari contact QR code.",
}); });
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,
@@ -671,20 +648,12 @@ export default class ContactQRScanShow extends Vue {
}); });
} }
async onCopyUrlToClipboard() { onCopyUrlToClipboard() {
const account = (await libsUtil.retrieveFullyDecryptedAccount( //this.onScanDetect([{ rawValue: this.qrValue }]); // good for testing
this.activeDid,
)) as Account;
const jwtUrl = await generateEndorserJwtUrlForAccount(
account,
this.isRegistered,
this.givenName,
this.profileImageUrl,
true,
);
useClipboard() useClipboard()
.copy(jwtUrl) .copy(this.qrValue)
.then(() => { .then(() => {
// console.log("Contact URL:", this.qrValue);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -802,16 +771,14 @@ 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.",
}, },
5000, 3000,
); );
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 = JSON.stringify(contact.contactMethods);
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",

View File

@@ -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-green-400" class="px-4 rounded-r bg-green-200 border border-l-0 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=""> <div class="w-full text-left">
<div v-if="!showGiveNumbers" class="flex items-center"> <div v-if="!showGiveNumbers">
<input <input
type="checkbox" type="checkbox"
:checked="contactsSelected.length === contacts.length" :checked="contactsSelected.length === contacts.length"
@@ -101,32 +101,52 @@
/> />
<button <button
v-if="!showGiveNumbers" v-if="!showGiveNumbers"
:class=" href=""
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
? 'text-md bg-gradient-to-b from-blue-400 to-blue-700 ' + ? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);'
'shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ' + : 'background-image: linear-gradient(to bottom, #94a3b8, #374151);'
'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 Copy Selections
</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="flex items-center gap-2"> <div class="w-full text-right">
<button <button
v-if="showGiveNumbers" href=""
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" 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 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()"
> >
@@ -139,24 +159,6 @@
}} }}
<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>
@@ -164,7 +166,7 @@
<ul <ul
v-if="contacts.length > 0" v-if="contacts.length > 0"
id="listContacts" id="listContacts"
class="border-t border-slate-300 my-2" class="border-t border-slate-300 mt-1"
> >
<li <li
v-for="contact in filteredContacts()" v-for="contact in filteredContacts()"
@@ -172,125 +174,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="flex items-center justify-between gap-3"> <div class="grow overflow-hidden">
<div class="flex overflow-hidden min-w-0 items-center gap-3"> <div class="flex items-center justify-between gap-3">
<input <div class="flex items-center gap-3">
v-if="!showGiveNumbers" <input
type="checkbox" v-if="!showGiveNumbers"
:checked="contactsSelected.includes(contact.did)" type="checkbox"
class="ml-2 h-6 w-6 flex-shrink-0" :checked="contactsSelected.includes(contact.did)"
data-testId="contactCheckOne" class="ml-2 h-6 w-6 flex-shrink-0"
@click=" data-testId="contactCheckOne"
contactsSelected.includes(contact.did) @click="
? contactsSelected.splice( contactsSelected.includes(contact.did)
contactsSelected.indexOf(contact.did), ? contactsSelected.splice(
1, contactsSelected.indexOf(contact.did),
) 1,
: contactsSelected.push(contact.did) )
" : contactsSelected.push(contact.did)
/> "
/>
<EntityIcon <div
:contact="contact" class="flex-shrink-0 w-12 h-12 flex items-center justify-center"
:icon-size="48" >
class="shrink-0 align-text-bottom border border-slate-300 rounded cursor-pointer overflow-hidden" <EntityIcon
@click="showLargeIdenticon = contact" :contact="contact"
/> :icon-size="48"
class="inline-block align-text-bottom border border-slate-300 rounded cursor-pointer overflow-hidden"
@click="showLargeIdenticon = contact"
/>
</div>
<div class="overflow-hidden"> <h2 class="text-base font-semibold w-1/3 truncate flex-shrink-0">
<h2 class="text-base font-semibold truncate"> {{ contactNameNonBreakingSpace(contact.name) }}
<router-link
:to="{
path: '/did/' + encodeURIComponent(contact.did),
}"
title="See more about this person"
>
{{ contactNameNonBreakingSpace(contact.name) }}
</router-link>
</h2> </h2>
<div class="flex gap-1.5 items-center overflow-hidden"> <span>
<router-link <div class="flex gap-2 items-center">
:to="{ <router-link
path: '/did/' + encodeURIComponent(contact.did), :to="{
}" path: '/did/' + encodeURIComponent(contact.did),
title="See more about this person" }"
> title="See more about this person"
<font-awesome >
icon="circle-info" <font-awesome
class="text-base text-blue-500" icon="circle-info"
/> class="text-xl text-blue-500"
</router-link> />
</router-link>
<span class="text-xs truncate">{{ contact.did }}</span> <span class="text-sm overflow-hidden">{{
</div> libsUtil.shortDid(contact.did)
<div class="text-sm"> }}</span>
{{ contact.notes }} </div>
</div> <div class="text-sm">
</div> {{ contact.notes }}
</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>
<button <div
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" v-if="showGiveNumbers && contact.did != activeDid"
data-testId="offerButton" class="flex gap-2 items-center"
@click="openOfferDialog(contact.did, contact.name)"
> >
Offer <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-l-md"
: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>
<router-link <button
:to="{ 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"
name: 'contact-amounts', :title="givenByMeDescriptions[contact.did] || ''"
query: { contactDid: contact.did }, @click="confirmShowGiftedDialog(activeDid, 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" To:
title="See more given activity" <br />
> {{
<font-awesome icon="file-lines" class="fa-fw" /> /* eslint-disable prettier/prettier */
</router-link> showGiveTotals
? ((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>
@@ -312,18 +314,16 @@
/> />
<button <button
v-if="!showGiveNumbers" v-if="!showGiveNumbers"
:class=" href=""
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
? 'text-md bg-gradient-to-b from-blue-400 to-blue-700 ' + ? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);'
'shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ' + : 'background-image: linear-gradient(to bottom, #94a3b8, #374151);'
'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 Copy Selections
</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 /deep-link/contact-import/:jwt with a JWT that has an array of contacts // For external links, use /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.updateDidSpecificSettings(this.activeDid, { await databaseUtil.updateAccountSettings(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,
}, },
-1, 5000,
); );
} }
// 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,9 +933,45 @@ export default class ContactsView extends Vue {
} }
private async addContactFromEndorserMobileLine( private async addContactFromEndorserMobileLine(
lineRaw: string, line: string,
): Promise<IndexableType> { ): Promise<IndexableType> {
const newContact = libsUtil.csvLineToContact(lineRaw); // Note that Endorser Mobile puts name first, then did, etc.
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>,
@@ -962,6 +998,8 @@ 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
@@ -1122,7 +1160,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.",
}, },
-1, 5000,
); );
} }
} catch (error) { } catch (error) {
@@ -1156,7 +1194,7 @@ export default class ContactsView extends Vue {
title: "Registration Error", title: "Registration Error",
text: userMessage, text: userMessage,
}, },
-1, 5000,
); );
} }
} }
@@ -1177,6 +1215,7 @@ 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(
{ {
@@ -1392,11 +1431,14 @@ 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 = const contactsJwtUrl = APP_SERVER + "/contact-import/" + contactsJwt;
APP_SERVER + "/deep-link/contact-import/" + contactsJwt;
useClipboard() useClipboard()
.copy(contactsJwtUrl) .copy(contactsJwtUrl)
.then(() => { .then(() => {

View File

@@ -66,14 +66,9 @@ const formattedPath = computed(() => {
const path = originalPath.value.replace(/^\/+/, ""); const path = originalPath.value.replace(/^\/+/, "");
// Log for debugging // Log for debugging
logger.log( logger.log("Original Path:", originalPath.value);
"[DeepLinkError] Original Path:", logger.log("Route Params:", route.params);
originalPath.value, logger.log("Route Query:", route.query);
"Route Params:",
route.params,
"Route Query:",
route.query,
);
return path; return path;
}); });

View File

@@ -1,227 +0,0 @@
<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>

View File

@@ -523,7 +523,9 @@ 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: " + errorStringForLog(e)); logger.error("Error with search all:", e);
// this sometimes gives different information
logger.error("Error with search all (error added): " + e);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -615,7 +617,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: " + errorStringForLog(e)); logger.error("Error with search local:", e);
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -786,7 +788,7 @@ export default class DiscoverView extends Vue {
const route = { const route = {
path: this.isProjectsActive path: this.isProjectsActive
? "/project/" + encodeURIComponent(id) ? "/project/" + encodeURIComponent(id)
: "/user-profile/" + encodeURIComponent(id), : "/userProfile/" + encodeURIComponent(id),
}; };
this.$router.push(route); this.$router.push(route);
} }

View File

@@ -826,7 +826,7 @@ export default class GiftedDetails extends Vue {
} }
if (!result.success) { if (!result.success) {
const errorMessage = result.error; 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(
{ {
@@ -899,6 +899,19 @@ export default class GiftedDetails 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(
{ {

View File

@@ -24,11 +24,11 @@
<!-- eslint-disable prettier/prettier max-len --> <!-- eslint-disable prettier/prettier max-len -->
<div> <div>
<p> <p>
This app focuses on raw gratitude, using it to build cool things together with your network. This app focuses on gifts & gratitude, using them 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 again, If you'd like to see the page-by-page help,
<span <span
class="text-blue-500 cursor-pointer" class="text-blue-500 cursor-pointer"
@click="unsetFinishedOnboarding()" @click="unsetFinishedOnboarding()"
@@ -37,16 +37,14 @@
<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 We are building networks of people who want to grow good society from the ground up, using modern
modern technology that connects people peer-to-peer. technology that connects people peer-to-peer.
First of all, let's showcase gratitude: see what people have given, and recognize gifts First of all, let's showcase gratitude: see what people have given, and recognize
you've seen. This is done in a way that leaves a permanent record -- one that provably gifts you've seen. This is done in a way that leaves a permanent record -- one that
came from you, and one that the recipient can prove they were mentioned. came from you, and one that the recipient can prove it was for them. This can be
This can be personally gratifying, but it extends to broader work: volunteers get personally gratifying, but it extends to broader work: volunteers get
confirmation of activity, and they can selectively show off their contributions and confirmation of activity, and they can selectively show off their contributions
network. and 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 --
@@ -557,6 +555,9 @@
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>
@@ -566,28 +567,6 @@
>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>
@@ -624,7 +603,6 @@ 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.
@@ -644,7 +622,7 @@ export default class HelpView extends Vue {
} }
if (settings.activeDid) { if (settings.activeDid) {
await databaseUtil.updateDidSpecificSettings(settings.activeDid, { await databaseUtil.updateAccountSettings(settings.activeDid, {
finishedOnboarding: false, finishedOnboarding: false,
}); });
if (USE_DEXIE_DB) { if (USE_DEXIE_DB) {

View File

@@ -519,6 +519,7 @@ 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(
@@ -551,6 +552,9 @@ 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}`,
@@ -577,6 +581,9 @@ 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}`,
@@ -623,7 +630,7 @@ export default class HomeView extends Vue {
this.activeDid, this.activeDid,
); );
if (resp.status === 200) { if (resp.status === 200) {
await databaseUtil.updateDidSpecificSettings(this.activeDid, { await databaseUtil.updateAccountSettings(this.activeDid, {
isRegistered: true, isRegistered: true,
...(await databaseUtil.retrieveSettingsForActiveAccount()), ...(await databaseUtil.retrieveSettingsForActiveAccount()),
}); });
@@ -634,6 +641,9 @@ 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(
@@ -675,6 +685,11 @@ 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(
@@ -770,7 +785,7 @@ export default class HomeView extends Vue {
if (USE_DEXIE_DB) { if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount(); settings = await retrieveSettingsForActiveAccount();
} }
await databaseUtil.updateDidSpecificSettings(this.activeDid, { await databaseUtil.updateAccountSettings(this.activeDid, {
apiServer: this.apiServer, apiServer: this.apiServer,
isRegistered: true, isRegistered: true,
...settings, ...settings,
@@ -1828,7 +1843,7 @@ export default class HomeView extends Vue {
this.axios, this.axios,
); );
if (result.success) { if (result.type === "success") {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",

View File

@@ -79,8 +79,7 @@ import {
newIdentifier, newIdentifier,
nextDerivationPath, nextDerivationPath,
} from "../libs/crypto"; } from "../libs/crypto";
import * as databaseUtil from "../db/databaseUtil"; import { accountsDBPromise, db } from "../db/index";
import { db } from "../db/index";
import { MASTER_SETTINGS_KEY } from "../db/tables/settings"; import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
import { import {
retrieveAllAccountsMetadata, retrieveAllAccountsMetadata,
@@ -165,15 +164,23 @@ 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,

View File

@@ -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="invite.inviteIdentifier" :title="inviteLink(invite.jwt)"
@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 + "/deep-link/invite-one-accept/" + jwt; return APP_SERVER + "/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",
{ inviteJwt, notes, expiresAt }, { inviteIdentifier, inviteJwt, notes, expiresAt },
{ headers }, { headers },
); );
const newInvite = { const newInvite = {

View File

@@ -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.updateDidSpecificSettings(this.activeDid, { await databaseUtil.updateAccountSettings(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.updateDidSpecificSettings(this.activeDid, { await databaseUtil.updateAccountSettings(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.updateDidSpecificSettings(this.activeDid, { await databaseUtil.updateAccountSettings(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.updateDidSpecificSettings(this.activeDid, { await databaseUtil.updateAccountSettings(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.updateDidSpecificSettings(this.activeDid, { await databaseUtil.updateAccountSettings(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.updateDidSpecificSettings(this.activeDid, { await databaseUtil.updateAccountSettings(this.activeDid, {
lastAckedOfferToUserProjectsJwtId: lastAckedOfferToUserProjectsJwtId:
this.lastAckedOfferToUserProjectsJwtId, this.lastAckedOfferToUserProjectsJwtId,
}); });

View File

@@ -720,7 +720,7 @@ export default class OnboardMeetingView extends Vue {
onboardMeetingMembersLink(): string { onboardMeetingMembersLink(): string {
if (this.currentMeeting) { if (this.currentMeeting) {
return `${APP_SERVER}/deep-link/onboard-meeting-members/${this.currentMeeting?.groupId}?password=${encodeURIComponent( return `${APP_SERVER}/onboard-meeting-members/${this.currentMeeting?.groupId}?password=${encodeURIComponent(
this.currentMeeting?.password || "", this.currentMeeting?.password || "",
)}`; )}`;
} }

View File

@@ -27,12 +27,6 @@
> >
<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>
@@ -58,28 +52,16 @@
icon="user" icon="user"
class="fa-fw text-slate-400" class="fa-fw text-slate-400"
></font-awesome> ></font-awesome>
<span class="truncate inline-block max-w-[calc(100%-2rem)]"> {{ issuerInfoObject?.displayName }}
{{ issuerInfoObject?.displayName }} <span v-if="!serverUtil.isEmptyOrHiddenDid(issuer)">
</span> <a :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"
/> />
</router-link> </a>
</span> </span>
<span v-if="serverUtil.isHiddenDid(issuer)" class="ml-1"> <span v-else-if="serverUtil.isHiddenDid(issuer)">
<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"
@@ -123,7 +105,7 @@
class="fa-fw text-slate-400" class="fa-fw text-slate-400"
></font-awesome> ></font-awesome>
<a <a
:href="ensureScheme(url)" :href="addScheme(url)"
target="_blank" target="_blank"
class="underline text-blue-500" class="underline text-blue-500"
> >
@@ -642,7 +624,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 { APP_SERVER, NotificationIface, USE_DEXIE_DB } from "../constants/app"; import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
import * as databaseUtil from "../db/databaseUtil"; import * as databaseUtil from "../db/databaseUtil";
import { import {
db, db,
@@ -656,7 +638,6 @@ 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
@@ -853,28 +834,6 @@ 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;
@@ -1337,7 +1296,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
ensureScheme(url: string) { addScheme(url: string) {
if (!libsUtil.isGlobalUri(url)) { if (!libsUtil.isGlobalUri(url)) {
return "https://" + url; return "https://" + url;
} }
@@ -1466,7 +1425,7 @@ export default class ProjectViewView extends Vue {
this.apiServer, this.apiServer,
this.axios, this.axios,
); );
if (result.success) { if (result.type === "success") {
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -1498,13 +1457,7 @@ 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,

View File

@@ -155,7 +155,7 @@ import { Contact } from "../db/tables/contacts";
import { import {
GenericCredWrapper, GenericCredWrapper,
GenericVerifiableCredential, GenericVerifiableCredential,
CreateAndSubmitClaimResult, ErrorResult,
} from "../interfaces"; } from "../interfaces";
import { import {
BVC_MEETUPS_PROJECT_CLAIM_ID, BVC_MEETUPS_PROJECT_CLAIM_ID,
@@ -298,29 +298,28 @@ 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: PromiseSettledResult<CreateAndSubmitClaimResult>[] = const confirmResults = await Promise.allSettled(
await Promise.allSettled( this.claimsToConfirmSelected.map(async (jwtId) => {
this.claimsToConfirmSelected.map(async (jwtId) => { const record = this.claimsToConfirm.find(
const record = this.claimsToConfirm.find( (claim) => claim.id === jwtId,
(claim) => claim.id === jwtId, );
); if (!record) {
if (!record) { return { type: "error", error: "Record not found." };
return { success: false, error: "Record not found." }; }
} return createAndSubmitConfirmation(
return createAndSubmitConfirmation( this.activeDid,
this.activeDid, record.claim as GenericVerifiableCredential,
record.claim as GenericVerifiableCredential, record.id,
record.id, record.handleId,
record.handleId, this.apiServer,
this.apiServer, axios,
axios, );
); }),
}), );
);
// check for any rejected confirmations // check for any rejected confirmations
const confirmsSucceeded = confirmResults.filter( const confirmsSucceeded = confirmResults.filter(
// 'fulfilled' is the status in a successful PromiseFulfilledResult (result) =>
(result) => result.status === "fulfilled" && result.value.success, result.status === "fulfilled" && result.value.type === "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);
@@ -354,7 +353,7 @@ export default class QuickActionBvcBeginView extends Vue {
undefined, undefined,
BVC_MEETUPS_PROJECT_CLAIM_ID, BVC_MEETUPS_PROJECT_CLAIM_ID,
); );
giveSucceeded = giveResult.success; giveSucceeded = giveResult.type === "success";
if (!giveSucceeded) { if (!giveSucceeded) {
logger.error("Error sending give:", giveResult); logger.error("Error sending give:", giveResult);
this.$notify( this.$notify(
@@ -363,7 +362,7 @@ export default class QuickActionBvcBeginView extends Vue {
type: "danger", type: "danger",
title: "Error", title: "Error",
text: text:
(giveResult as CreateAndSubmitClaimResult)?.error || (giveResult as ErrorResult)?.error?.userMessage ||
"There was an error sending that give.", "There was an error sending that give.",
}, },
5000, 5000,

View File

@@ -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: searchBoxes as unknown, // Type assertion for Dexie compatibility searchBoxes: [newSearchBox],
}); });
} }
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: "[]" as unknown as string, // Type assertion for Dexie compatibility searchBoxes: [],
filterFeedByNearby: false, filterFeedByNearby: false,
}); });
} }

View File

@@ -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 click on it, or paste it in the box on their 'Contacts' screen.", text: "Your contact info was copied to the clipboard. Have them paste it in the box on their 'Contacts' screen.",
}, },
5000, 5000,
); );

View File

@@ -16,7 +16,6 @@
</button> </button>
Individual Profile Individual Profile
</h1> </h1>
<div class="text-sm text-center text-slate-500"></div>
</div> </div>
<!-- Loading Animation --> <!-- Loading Animation -->
@@ -33,12 +32,6 @@
<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 }}
@@ -107,7 +100,6 @@ 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,
@@ -121,7 +113,6 @@ 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,
@@ -195,10 +186,6 @@ 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");
} }
@@ -217,22 +204,5 @@ 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>