Compare commits
30 Commits
ui-fixes-2
...
deep-links
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb2a4ab76e | ||
|
|
048dded278 | ||
| e240c2940a | |||
| 54dca9e745 | |||
| 9f0fed0a60 | |||
| 0d152adbf2 | |||
| cead308800 | |||
| 676a301331 | |||
| d6db81cc36 | |||
|
|
f2ddcd2541 | ||
| fb81f7b96e | |||
| a23416ead1 | |||
| 530c7c1a13 | |||
| f255ea389b | |||
| 0d343b9877 | |||
| df06100c32 | |||
|
|
ac5ddfc6f2 | ||
|
|
89b3f30466 | ||
|
|
3cb5cc096b | ||
|
|
5df560154f | ||
|
|
c1aa522e6c | ||
| a082469a01 | |||
|
|
3544d7278d | ||
|
|
d3110506ea | ||
| 8609f8458d | |||
| 8f5c34bc5f | |||
| b0d61b95ea | |||
| af7bd236a3 | |||
| d719338bcc | |||
| 6ddf2d1012 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -55,3 +55,4 @@ build_logs/
|
||||
icons
|
||||
|
||||
|
||||
android/app/src/main/res/
|
||||
69
BUILDING.md
69
BUILDING.md
@@ -9,19 +9,6 @@ For a quick dev environment setup, use [pkgx](https://pkgx.dev).
|
||||
- Node.js (LTS version recommended)
|
||||
- npm (comes with Node.js)
|
||||
- Git
|
||||
- For Android builds: Android Studio with SDK installed
|
||||
- For iOS builds: macOS with Xcode and ruby gems & bundle
|
||||
- `pkgx +rubygems.org sh`
|
||||
|
||||
- ... and you may have to fix these, especially with pkgx
|
||||
|
||||
```bash
|
||||
gem_path=$(which gem)
|
||||
shortened_path="${gem_path:h:h}"
|
||||
export GEM_HOME=$shortened_path
|
||||
export GEM_PATH=$shortened_path
|
||||
```
|
||||
|
||||
- For desktop builds: Additional build tools based on your OS
|
||||
|
||||
## Forks
|
||||
@@ -326,7 +313,30 @@ npm run build:electron-prod && npm run electron:start
|
||||
|
||||
Prerequisites: macOS with Xcode installed
|
||||
|
||||
1. Build the web assets:
|
||||
#### First-time iOS Configuration
|
||||
|
||||
- Generate certificates inside XCode.
|
||||
|
||||
- Right-click on App and under Signing & Capabilities set the Team.
|
||||
|
||||
#### Each Release
|
||||
|
||||
0. First time (or if dependencies change):
|
||||
|
||||
- `pkgx +rubygems.org sh`
|
||||
|
||||
- ... and you may have to fix these, especially with pkgx:
|
||||
|
||||
```bash
|
||||
gem_path=$(which gem)
|
||||
shortened_path="${gem_path:h:h}"
|
||||
export GEM_HOME=$shortened_path
|
||||
export GEM_PATH=$shortened_path
|
||||
```
|
||||
|
||||
1. Check the iOS flag isIOS in CapacitorPlatformService (currently hard-coded for iOS build).
|
||||
|
||||
2. Build the web assets:
|
||||
|
||||
```bash
|
||||
rm -rf dist
|
||||
@@ -334,7 +344,7 @@ Prerequisites: macOS with Xcode installed
|
||||
npm run build:capacitor
|
||||
```
|
||||
|
||||
2. Update iOS project with latest build:
|
||||
3. Update iOS project with latest build:
|
||||
|
||||
```bash
|
||||
npx cap sync ios
|
||||
@@ -342,7 +352,7 @@ Prerequisites: macOS with Xcode installed
|
||||
|
||||
- If that fails with "Could not find..." then look at the "gem_path" instructions above.
|
||||
|
||||
3. Copy the assets:
|
||||
4. Copy the assets:
|
||||
|
||||
```bash
|
||||
# It makes no sense why capacitor-assets will not run without these but it actually changes the contents.
|
||||
@@ -353,15 +363,14 @@ Prerequisites: macOS with Xcode installed
|
||||
npx capacitor-assets generate --ios
|
||||
```
|
||||
|
||||
4. Bump the version to match Android:
|
||||
4. Bump the version to match Android & package.json:
|
||||
|
||||
```
|
||||
cd ios/App
|
||||
xcrun agvtool new-version 21
|
||||
xcrun agvtool new-version 30
|
||||
# Unfortunately this edits Info.plist directly.
|
||||
#xcrun agvtool new-marketing-version 0.4.5
|
||||
cat App.xcodeproj/project.pbxproj | sed "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 0.4.7;/g" > temp
|
||||
mv temp App.xcodeproj/project.pbxproj
|
||||
cat App.xcodeproj/project.pbxproj | sed "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 0.5.4;/g" > temp && mv temp App.xcodeproj/project.pbxproj
|
||||
cd -
|
||||
```
|
||||
|
||||
@@ -377,7 +386,7 @@ Prerequisites: macOS with Xcode installed
|
||||
|
||||
7. Release
|
||||
|
||||
* Under "General" we want to rename a bunch of things to "Time Safari"
|
||||
* Someday: Under "General" we want to rename a bunch of things to "Time Safari"
|
||||
* Choose Product -> Destination -> Any iOS Device
|
||||
* Choose Product -> Archive
|
||||
* This will trigger a build and take time, needing user's "login" keychain password (user's login password), repeatedly.
|
||||
@@ -389,15 +398,11 @@ Prerequisites: macOS with Xcode installed
|
||||
* You'll probably have to "Manage" something about encryption, disallowed in France.
|
||||
* Then "Save" and "Add to Review" and "Resubmit to App Review".
|
||||
|
||||
#### First-time iOS Configuration
|
||||
|
||||
- Generate certificates inside XCode.
|
||||
|
||||
- Right-click on App and under Signing & Capabilities set the Team.
|
||||
8. Revert the iOS flag isIOS in CapacitorPlatformService.
|
||||
|
||||
### Android Build
|
||||
|
||||
Prerequisites: Android Studio with SDK installed
|
||||
Prerequisites: Android Studio with Java SDK installed
|
||||
|
||||
1. Build the web assets:
|
||||
|
||||
@@ -419,7 +424,7 @@ Prerequisites: Android Studio with SDK installed
|
||||
npx capacitor-assets generate --android
|
||||
```
|
||||
|
||||
4. Bump version to match iOS: android/app/build.gradle
|
||||
4. Bump version to match iOS & package.json: android/app/build.gradle
|
||||
|
||||
5. Open the project in Android Studio:
|
||||
|
||||
@@ -452,7 +457,9 @@ Prerequisites: Android Studio with SDK installed
|
||||
* Then `bundleRelease`:
|
||||
|
||||
```bash
|
||||
cd android
|
||||
./gradlew bundleRelease -Dlint.baselines.continue=true
|
||||
cd -
|
||||
```
|
||||
|
||||
... and find your `aab` file at app/build/outputs/bundle/release
|
||||
@@ -468,7 +475,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.
|
||||
|
||||
|
||||
## First-time Android Configuration for deep links
|
||||
## Android Configuration for deep links
|
||||
|
||||
You must add the following intent filter to the `android/app/src/main/AndroidManifest.xml` file:
|
||||
|
||||
@@ -479,4 +486,6 @@ You must add the following intent filter to the `android/app/src/main/AndroidMan
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="timesafari" />
|
||||
</intent-filter>
|
||||
```
|
||||
```
|
||||
|
||||
... though when we tried that most recently it failed to 'build' the APK with: http(s) scheme and host attribute are missing, but are required for Android App Links [AppLinkUrlError]
|
||||
|
||||
@@ -31,8 +31,8 @@ android {
|
||||
applicationId "app.timesafari.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 23
|
||||
versionName "0.4.8"
|
||||
versionCode 30
|
||||
versionName "0.5.4"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
}
|
||||
},
|
||||
"ios": {
|
||||
"contentInset": "always",
|
||||
"contentInset": "never",
|
||||
"allowsLinkPreview": true,
|
||||
"scrollEnabled": true,
|
||||
"limitsNavigationsToAppBoundDomains": true,
|
||||
|
||||
@@ -403,7 +403,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 23;
|
||||
CURRENT_PROJECT_VERSION = 30;
|
||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||
ENABLE_APP_SANDBOX = NO;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
@@ -413,7 +413,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.4.8;
|
||||
MARKETING_VERSION = 0.5.4;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -430,7 +430,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 23;
|
||||
CURRENT_PROJECT_VERSION = 30;
|
||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||
ENABLE_APP_SANDBOX = NO;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
@@ -440,7 +440,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.4.8;
|
||||
MARKETING_VERSION = 0.5.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||
|
||||
@@ -49,5 +49,16 @@
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>app.timesafari</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>timesafari</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
1217
package-lock.json
generated
1217
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "timesafari",
|
||||
"version": "0.4.8",
|
||||
"version": "0.5.4",
|
||||
"description": "Time Safari Application",
|
||||
"author": {
|
||||
"name": "Time Safari Team"
|
||||
|
||||
@@ -49,7 +49,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a class="cursor-pointer" @click="$emit('loadClaim', record.jwtId)" data-testid="circle-info-link">
|
||||
<a
|
||||
class="cursor-pointer"
|
||||
data-testid="circle-info-link"
|
||||
@click="$emit('loadClaim', record.jwtId)"
|
||||
>
|
||||
<font-awesome icon="circle-info" class="fa-fw text-slate-500" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -104,7 +104,6 @@ import { USE_DEXIE_DB } from "@/constants/app";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
||||
import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
@@ -143,19 +142,23 @@ export default class FeedFilters extends Vue {
|
||||
async toggleHasVisibleDid() {
|
||||
this.settingChanged = true;
|
||||
this.hasVisibleDid = !this.hasVisibleDid;
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
await databaseUtil.updateDefaultSettings({
|
||||
filterFeedByVisible: this.hasVisibleDid,
|
||||
});
|
||||
|
||||
if (USE_DEXIE_DB) {
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
filterFeedByVisible: this.hasVisibleDid,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async toggleNearby() {
|
||||
this.settingChanged = true;
|
||||
this.isNearby = !this.isNearby;
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
await platformService.dbExec(
|
||||
`UPDATE settings SET filterFeedByNearby = ? WHERE id = ?`,
|
||||
[this.isNearby, MASTER_SETTINGS_KEY],
|
||||
);
|
||||
await databaseUtil.updateDefaultSettings({
|
||||
filterFeedByNearby: this.isNearby,
|
||||
});
|
||||
|
||||
if (USE_DEXIE_DB) {
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
@@ -169,11 +172,10 @@ export default class FeedFilters extends Vue {
|
||||
this.settingChanged = true;
|
||||
}
|
||||
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
await platformService.dbExec(
|
||||
`UPDATE settings SET filterFeedByNearby = ? AND filterFeedByVisible = ? WHERE id = ?`,
|
||||
[false, false, MASTER_SETTINGS_KEY],
|
||||
);
|
||||
await databaseUtil.updateDefaultSettings({
|
||||
filterFeedByNearby: false,
|
||||
filterFeedByVisible: false,
|
||||
});
|
||||
|
||||
if (USE_DEXIE_DB) {
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
@@ -191,11 +193,10 @@ export default class FeedFilters extends Vue {
|
||||
this.settingChanged = true;
|
||||
}
|
||||
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
await platformService.dbExec(
|
||||
`UPDATE settings SET filterFeedByNearby = ? AND filterFeedByVisible = ? WHERE id = ?`,
|
||||
[true, true, MASTER_SETTINGS_KEY],
|
||||
);
|
||||
await databaseUtil.updateDefaultSettings({
|
||||
filterFeedByNearby: true,
|
||||
filterFeedByVisible: true,
|
||||
});
|
||||
|
||||
if (USE_DEXIE_DB) {
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
|
||||
@@ -321,7 +321,7 @@ export default class GiftedDialog extends Vue {
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
const errorMessage = this.getGiveCreationErrorMessage(result);
|
||||
const errorMessage = result.error;
|
||||
logger.error("Error with give creation result:", result);
|
||||
this.$notify(
|
||||
{
|
||||
@@ -367,19 +367,6 @@ export default class GiftedDialog 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
|
||||
getGiveCreationErrorMessage(result: any) {
|
||||
return (
|
||||
result.error?.userMessage ||
|
||||
result.error?.error ||
|
||||
result.response?.data?.error?.message
|
||||
);
|
||||
}
|
||||
|
||||
explainData() {
|
||||
this.$notify(
|
||||
{
|
||||
|
||||
@@ -227,6 +227,7 @@ export default class GivenPrompts extends Vue {
|
||||
|
||||
let someContactDbIndex = Math.floor(Math.random() * this.numContacts);
|
||||
let count = 0;
|
||||
|
||||
// as long as the index has an entry, loop
|
||||
while (
|
||||
this.shownContactDbIndices[someContactDbIndex] != null &&
|
||||
@@ -245,9 +246,8 @@ export default class GivenPrompts extends Vue {
|
||||
[someContactDbIndex],
|
||||
);
|
||||
if (result) {
|
||||
this.currentContact = databaseUtil.mapQueryResultToValues(result)[
|
||||
someContactDbIndex
|
||||
] as unknown as Contact;
|
||||
const mappedContacts = databaseUtil.mapQueryResultToValues(result);
|
||||
this.currentContact = mappedContacts[0] as unknown as Contact;
|
||||
}
|
||||
if (USE_DEXIE_DB) {
|
||||
await db.open();
|
||||
|
||||
@@ -48,15 +48,15 @@
|
||||
<span>
|
||||
{{ didInfo(visDid) }}
|
||||
<span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)">
|
||||
<a
|
||||
:href="`/did/${visDid}`"
|
||||
<router-link
|
||||
:to="{ path: '/did/' + encodeURIComponent(visDid) }"
|
||||
class="text-blue-500"
|
||||
>
|
||||
<font-awesome
|
||||
icon="arrow-up-right-from-square"
|
||||
class="fa-fw"
|
||||
/>
|
||||
</a>
|
||||
</router-link>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -250,7 +250,7 @@ export default class OfferDialog extends Vue {
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
const errorMessage = this.getOfferCreationErrorMessage(result);
|
||||
const errorMessage = result.error;
|
||||
logger.error("Error with offer creation result:", result);
|
||||
this.$notify(
|
||||
{
|
||||
@@ -290,21 +290,6 @@ export default class OfferDialog extends Vue {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions for readability
|
||||
|
||||
/**
|
||||
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
|
||||
* @returns best guess at an error message
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getOfferCreationErrorMessage(result: any) {
|
||||
return (
|
||||
serverMessageForUser(result) ||
|
||||
result.error?.userMessage ||
|
||||
result.error?.error
|
||||
);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
</div>
|
||||
</h1>
|
||||
|
||||
The feed underneath this pop-up shows the latest contributions,
|
||||
some from people and some from projects.
|
||||
The feed underneath this pop-up shows the latest contributions, some from
|
||||
people and some from projects.
|
||||
|
||||
<p v-if="isRegistered" class="mt-4">
|
||||
You can now log things that you've seen:
|
||||
@@ -29,8 +29,7 @@
|
||||
button to express your appreciation for... whatever.
|
||||
</p>
|
||||
<p class="mt-4">
|
||||
Once someone registers you, you can log your
|
||||
appreciation, too.
|
||||
Once someone registers you, you can log your appreciation, too.
|
||||
</p>
|
||||
|
||||
<p class="mt-4">
|
||||
@@ -260,7 +259,7 @@ export default class OnboardingDialog extends Vue {
|
||||
this.visible = true;
|
||||
if (this.page === OnboardPage.Create) {
|
||||
// we'll assume that they've been through all the other pages
|
||||
await databaseUtil.updateAccountSettings(this.activeDid, {
|
||||
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
|
||||
finishedOnboarding: true,
|
||||
});
|
||||
if (USE_DEXIE_DB) {
|
||||
@@ -274,7 +273,7 @@ export default class OnboardingDialog extends Vue {
|
||||
async onClickClose(done?: boolean, goHome?: boolean) {
|
||||
this.visible = false;
|
||||
if (done) {
|
||||
await databaseUtil.updateAccountSettings(this.activeDid, {
|
||||
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
|
||||
finishedOnboarding: true,
|
||||
});
|
||||
if (USE_DEXIE_DB) {
|
||||
|
||||
@@ -8,11 +8,7 @@
|
||||
>
|
||||
<div class="h-full w-full object-contain" v-html="generateIcon()" />
|
||||
</a>
|
||||
<div
|
||||
v-else
|
||||
class="h-full w-full object-contain"
|
||||
v-html="generateIcon()"
|
||||
/>
|
||||
<div v-else class="h-full w-full object-contain" v-html="generateIcon()" />
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { toSvg } from "jdenticon";
|
||||
|
||||
@@ -38,14 +38,14 @@ export default class TopMessage extends Vue {
|
||||
settings.apiServer !== AppString.PROD_ENDORSER_API_SERVER
|
||||
) {
|
||||
const didPrefix = settings.activeDid?.slice(11, 15);
|
||||
this.message = "You're linked to a non-prod server, user " + didPrefix;
|
||||
this.message = "You're not using prod, user " + didPrefix;
|
||||
} else if (
|
||||
settings.warnIfProdServer &&
|
||||
settings.apiServer === AppString.PROD_ENDORSER_API_SERVER
|
||||
) {
|
||||
const didPrefix = settings.activeDid?.slice(11, 15);
|
||||
this.message =
|
||||
"You're linked to the production server, user " + didPrefix;
|
||||
"You are using prod, user " + didPrefix;
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
this.$notify(
|
||||
|
||||
@@ -33,18 +33,18 @@ export const APP_SERVER =
|
||||
|
||||
export const DEFAULT_ENDORSER_API_SERVER =
|
||||
import.meta.env.VITE_DEFAULT_ENDORSER_API_SERVER ||
|
||||
AppString.TEST_ENDORSER_API_SERVER;
|
||||
AppString.PROD_ENDORSER_API_SERVER;
|
||||
|
||||
export const DEFAULT_IMAGE_API_SERVER =
|
||||
import.meta.env.VITE_DEFAULT_IMAGE_API_SERVER ||
|
||||
AppString.TEST_IMAGE_API_SERVER;
|
||||
AppString.PROD_IMAGE_API_SERVER;
|
||||
|
||||
export const DEFAULT_PARTNER_API_SERVER =
|
||||
import.meta.env.VITE_DEFAULT_PARTNER_API_SERVER ||
|
||||
AppString.TEST_PARTNER_API_SERVER;
|
||||
AppString.PROD_PARTNER_API_SERVER;
|
||||
|
||||
export const DEFAULT_PUSH_SERVER =
|
||||
import.meta.env.VITE_DEFAULT_PUSH_SERVER || "https://timesafari.app";
|
||||
import.meta.env.VITE_DEFAULT_PUSH_SERVER || AppString.PROD_PUSH_SERVER;
|
||||
|
||||
export const IMAGE_TYPE_PROFILE = "profile";
|
||||
|
||||
|
||||
@@ -37,7 +37,20 @@ export async function updateDefaultSettings(
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateAccountSettings(
|
||||
export async function insertDidSpecificSettings(
|
||||
did: string,
|
||||
settings: Partial<Settings> = {},
|
||||
): Promise<boolean> {
|
||||
const platform = PlatformServiceFactory.getInstance();
|
||||
const { sql, params } = generateInsertStatement(
|
||||
{ ...settings, accountDid: did }, // make sure accountDid is set to the given value
|
||||
"settings",
|
||||
);
|
||||
const result = await platform.dbExec(sql, params);
|
||||
return result.changes === 1;
|
||||
}
|
||||
|
||||
export async function updateDidSpecificSettings(
|
||||
accountDid: string,
|
||||
settingsChanges: Settings,
|
||||
): Promise<boolean> {
|
||||
@@ -55,20 +68,7 @@ export async function updateAccountSettings(
|
||||
);
|
||||
|
||||
const updateResult = await platform.dbExec(updateSql, updateParams);
|
||||
|
||||
// If no record was updated, insert a new one
|
||||
if (updateResult.changes === 1) {
|
||||
return true;
|
||||
} else {
|
||||
const columns = Object.keys(settingsChanges);
|
||||
const values = Object.values(settingsChanges);
|
||||
const placeholders = values.map(() => "?").join(", ");
|
||||
|
||||
const insertSql = `INSERT INTO settings (${columns.join(", ")}) VALUES (${placeholders})`;
|
||||
const result = await platform.dbExec(insertSql, values);
|
||||
|
||||
return result.changes === 1;
|
||||
}
|
||||
return updateResult.changes === 1;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: Settings = {
|
||||
@@ -109,9 +109,6 @@ export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
|
||||
const defaultSettings = await retrieveSettingsForDefaultAccount();
|
||||
// If no active DID, return defaults
|
||||
if (!defaultSettings.activeDid) {
|
||||
logConsoleAndDb(
|
||||
"[databaseUtil] No active DID found, returning default settings",
|
||||
);
|
||||
return defaultSettings;
|
||||
}
|
||||
|
||||
@@ -124,9 +121,7 @@ export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
|
||||
);
|
||||
|
||||
if (!result?.values?.length) {
|
||||
logConsoleAndDb(
|
||||
`[databaseUtil] No account-specific settings found for ${defaultSettings.activeDid}`,
|
||||
);
|
||||
// we created DID-specific settings when generated or imported, so this shouldn't happen
|
||||
return defaultSettings;
|
||||
}
|
||||
|
||||
@@ -135,6 +130,7 @@ export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
|
||||
result.columns,
|
||||
result.values,
|
||||
)[0] as Settings;
|
||||
|
||||
const overrideSettingsFiltered = Object.fromEntries(
|
||||
Object.entries(overrideSettings).filter(([_, v]) => v !== null),
|
||||
);
|
||||
@@ -144,17 +140,7 @@ export async function retrieveSettingsForActiveAccount(): Promise<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 ${defaultSettings.activeDid}: ${error}`,
|
||||
true,
|
||||
);
|
||||
// Reset to empty array on parse failure
|
||||
settings.searchBoxes = [];
|
||||
}
|
||||
settings.searchBoxes = parseJsonField(settings.searchBoxes, []);
|
||||
}
|
||||
|
||||
return settings;
|
||||
@@ -254,6 +240,7 @@ export function generateInsertStatement(
|
||||
const values = Object.values(model).filter((value) => value !== undefined);
|
||||
const placeholders = values.map(() => "?").join(", ");
|
||||
const insertSql = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${placeholders})`;
|
||||
|
||||
return {
|
||||
sql: insertSql,
|
||||
params: values,
|
||||
@@ -325,3 +312,115 @@ export function mapColumnsToValues(
|
||||
return obj;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug function to inspect raw settings data in the database
|
||||
* This helps diagnose issues with data corruption or malformed JSON
|
||||
* @param did Optional DID to inspect specific account settings
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
export async function debugSettingsData(did?: string): Promise<void> {
|
||||
try {
|
||||
const platform = PlatformServiceFactory.getInstance();
|
||||
|
||||
// Get all settings records
|
||||
const allSettings = await platform.dbQuery("SELECT * FROM settings");
|
||||
|
||||
logConsoleAndDb(
|
||||
`[DEBUG] Total settings records: ${allSettings?.values?.length || 0}`,
|
||||
false,
|
||||
);
|
||||
|
||||
if (allSettings?.values?.length) {
|
||||
allSettings.values.forEach((row, index) => {
|
||||
const settings = mapColumnsToValues(allSettings.columns, [row])[0];
|
||||
logConsoleAndDb(`[DEBUG] Settings record ${index + 1}:`, false);
|
||||
logConsoleAndDb(`[DEBUG] - ID: ${settings.id}`, false);
|
||||
logConsoleAndDb(`[DEBUG] - accountDid: ${settings.accountDid}`, false);
|
||||
logConsoleAndDb(`[DEBUG] - activeDid: ${settings.activeDid}`, false);
|
||||
|
||||
if (settings.searchBoxes) {
|
||||
logConsoleAndDb(
|
||||
`[DEBUG] - searchBoxes type: ${typeof settings.searchBoxes}`,
|
||||
false,
|
||||
);
|
||||
logConsoleAndDb(
|
||||
`[DEBUG] - searchBoxes value: ${String(settings.searchBoxes)}`,
|
||||
false,
|
||||
);
|
||||
|
||||
// Try to parse it
|
||||
try {
|
||||
const parsed = JSON.parse(String(settings.searchBoxes));
|
||||
logConsoleAndDb(
|
||||
`[DEBUG] - searchBoxes parsed successfully: ${JSON.stringify(parsed)}`,
|
||||
false,
|
||||
);
|
||||
} catch (parseError) {
|
||||
logConsoleAndDb(
|
||||
`[DEBUG] - searchBoxes parse error: ${parseError}`,
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
logConsoleAndDb(
|
||||
`[DEBUG] - Full record: ${JSON.stringify(settings, null, 2)}`,
|
||||
false,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// If specific DID provided, also check accounts table
|
||||
if (did) {
|
||||
const account = await platform.dbQuery(
|
||||
"SELECT * FROM accounts WHERE did = ?",
|
||||
[did],
|
||||
);
|
||||
logConsoleAndDb(
|
||||
`[DEBUG] Account for ${did}: ${JSON.stringify(account, null, 2)}`,
|
||||
false,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logConsoleAndDb(`[DEBUG] Error inspecting settings data: ${error}`, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Platform-agnostic JSON parsing utility
|
||||
* Handles different SQLite implementations:
|
||||
* - Web SQLite (wa-sqlite/absurd-sql): Auto-parses JSON strings to objects
|
||||
* - Capacitor SQLite: Returns raw strings that need manual parsing
|
||||
*
|
||||
* @param value The value to parse (could be string or already parsed object)
|
||||
* @param defaultValue Default value if parsing fails
|
||||
* @returns Parsed object or default value
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
export function parseJsonField<T>(value: unknown, defaultValue: T): T {
|
||||
try {
|
||||
// If already an object (web SQLite auto-parsed), return as-is
|
||||
if (typeof value === "object" && value !== null) {
|
||||
return value as T;
|
||||
}
|
||||
|
||||
// If it's a string (Capacitor SQLite or fallback), parse it
|
||||
if (typeof value === "string") {
|
||||
return JSON.parse(value) as T;
|
||||
}
|
||||
|
||||
// If it's null/undefined, return default
|
||||
if (value === null || value === undefined) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
} catch (error) {
|
||||
logConsoleAndDb(
|
||||
`[databaseUtil] Failed to parse JSON field: ${error}`,
|
||||
true,
|
||||
);
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { AxiosResponse } from "axios";
|
||||
import { GiverReceiverInputInfo } from "../libs/util";
|
||||
import { ErrorResult, ResultWithType } from "./common";
|
||||
|
||||
export interface GiverOutputInfo {
|
||||
action: string;
|
||||
@@ -47,12 +45,3 @@ export interface ProviderInfo {
|
||||
*/
|
||||
linkConfirmed: boolean;
|
||||
}
|
||||
|
||||
// Type for createAndSubmitClaim result
|
||||
export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult;
|
||||
|
||||
// Update SuccessResult to use ClaimResult
|
||||
export interface SuccessResult extends ResultWithType {
|
||||
type: "success";
|
||||
response: AxiosResponse<ClaimResult>;
|
||||
}
|
||||
|
||||
@@ -15,10 +15,6 @@ export interface GenericCredWrapper<T extends GenericVerifiableCredential> {
|
||||
publicUrls?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface ResultWithType {
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface ErrorResponse {
|
||||
error?: {
|
||||
message?: string;
|
||||
@@ -30,11 +26,6 @@ export interface InternalError {
|
||||
userMessage?: string;
|
||||
}
|
||||
|
||||
export interface ErrorResult extends ResultWithType {
|
||||
type: "error";
|
||||
error: InternalError;
|
||||
}
|
||||
|
||||
export interface KeyMeta {
|
||||
did: string;
|
||||
publicKeyHex: string;
|
||||
|
||||
@@ -30,7 +30,7 @@ import { z } from "zod";
|
||||
// Add a union type of all valid route paths
|
||||
export const VALID_DEEP_LINK_ROUTES = [
|
||||
"user-profile",
|
||||
"project-details",
|
||||
"project",
|
||||
"onboard-meeting-setup",
|
||||
"invite-one-accept",
|
||||
"contact-import",
|
||||
@@ -61,7 +61,7 @@ export const deepLinkSchemas = {
|
||||
"user-profile": z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
"project-details": z.object({
|
||||
"project": z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
"onboard-meeting-setup": z.object({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export type {
|
||||
// From common.ts
|
||||
CreateAndSubmitClaimResult,
|
||||
GenericCredWrapper,
|
||||
GenericVerifiableCredential,
|
||||
KeyMeta,
|
||||
@@ -18,11 +19,6 @@ export type {
|
||||
RegisterActionClaim,
|
||||
} from "./claims";
|
||||
|
||||
export type {
|
||||
// From claims-result.ts
|
||||
CreateAndSubmitClaimResult,
|
||||
} from "./claims-result";
|
||||
|
||||
export type {
|
||||
// From records.ts
|
||||
PlanSummaryRecord,
|
||||
|
||||
@@ -979,7 +979,7 @@ export const createAndSubmitConfirmation = async (
|
||||
handleId: string | undefined,
|
||||
apiServer: string,
|
||||
axios: Axios,
|
||||
) => {
|
||||
): Promise<CreateAndSubmitClaimResult> => {
|
||||
const goodClaim = removeSchemaContext(
|
||||
removeVisibleToDids(
|
||||
addLastClaimOrHandleAsIdIfMissing(claim, lastClaimId, handleId),
|
||||
|
||||
@@ -44,6 +44,7 @@ import { logger } from "../utils/logger";
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
import { sha256 } from "ethereum-cryptography/sha256";
|
||||
import { IIdentifier } from "@veramo/core";
|
||||
import { insertDidSpecificSettings, parseJsonField } from "../db/databaseUtil";
|
||||
|
||||
export interface GiverReceiverInputInfo {
|
||||
did?: string;
|
||||
@@ -626,7 +627,9 @@ export const retrieveFullyDecryptedAccount = async (
|
||||
return result;
|
||||
};
|
||||
|
||||
export const retrieveAllAccountsMetadata = async (): Promise<AccountEncrypted[]> => {
|
||||
export const retrieveAllAccountsMetadata = async (): Promise<
|
||||
AccountEncrypted[]
|
||||
> => {
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
const dbAccounts = await platformService.dbQuery(`SELECT * FROM accounts`);
|
||||
const accounts = databaseUtil.mapQueryResultToValues(dbAccounts) as Account[];
|
||||
@@ -643,8 +646,12 @@ export const retrieveAllAccountsMetadata = async (): Promise<AccountEncrypted[]>
|
||||
// This is not accurate because they can't be decrypted, but we're removing Dexie anyway.
|
||||
const identityStr = JSON.stringify(identity);
|
||||
const encryptedAccount = {
|
||||
identityEncrBase64: sha256(new TextEncoder().encode(identityStr)).toString(),
|
||||
mnemonicEncrBase64: sha256(new TextEncoder().encode(account.mnemonic)).toString(),
|
||||
identityEncrBase64: sha256(
|
||||
new TextEncoder().encode(identityStr),
|
||||
).toString(),
|
||||
mnemonicEncrBase64: sha256(
|
||||
new TextEncoder().encode(account.mnemonic),
|
||||
).toString(),
|
||||
...metadata,
|
||||
};
|
||||
return encryptedAccount as AccountEncrypted;
|
||||
@@ -691,6 +698,7 @@ export async function saveNewIdentity(
|
||||
];
|
||||
await platformService.dbExec(sql, params);
|
||||
await databaseUtil.updateDefaultSettings({ activeDid: identity.did });
|
||||
await databaseUtil.insertDidSpecificSettings(identity.did);
|
||||
|
||||
if (USE_DEXIE_DB) {
|
||||
// one of the few times we use accountsDBPromise directly; try to avoid more usage
|
||||
@@ -704,6 +712,7 @@ export async function saveNewIdentity(
|
||||
publicKeyHex: identity.keys[0].publicKeyHex,
|
||||
});
|
||||
await updateDefaultSettings({ activeDid: identity.did });
|
||||
await insertDidSpecificSettings(identity.did);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Failed to update default settings:", error);
|
||||
@@ -726,7 +735,9 @@ export const generateSaveAndActivateIdentity = async (): Promise<string> => {
|
||||
const newId = newIdentifier(address, publicHex, privateHex, derivationPath);
|
||||
|
||||
await saveNewIdentity(newId, mnemonic, derivationPath);
|
||||
await databaseUtil.updateAccountSettings(newId.did, { isRegistered: false });
|
||||
await databaseUtil.updateDidSpecificSettings(newId.did, {
|
||||
isRegistered: false,
|
||||
});
|
||||
if (USE_DEXIE_DB) {
|
||||
await updateAccountSettings(newId.did, { isRegistered: false });
|
||||
}
|
||||
@@ -768,7 +779,7 @@ export const registerSaveAndActivatePasskey = async (
|
||||
): Promise<Account> => {
|
||||
const account = await registerAndSavePasskey(keyName);
|
||||
await databaseUtil.updateDefaultSettings({ activeDid: account.did });
|
||||
await databaseUtil.updateAccountSettings(account.did, {
|
||||
await databaseUtil.updateDidSpecificSettings(account.did, {
|
||||
isRegistered: false,
|
||||
});
|
||||
if (USE_DEXIE_DB) {
|
||||
@@ -856,7 +867,7 @@ export const contactToCsvLine = (contact: Contact): string => {
|
||||
|
||||
// Handle contactMethods array by stringifying it
|
||||
const contactMethodsStr = contact.contactMethods
|
||||
? escapeField(JSON.stringify(contact.contactMethods))
|
||||
? escapeField(JSON.stringify(parseJsonField(contact.contactMethods, [])))
|
||||
: "";
|
||||
|
||||
const fields = [
|
||||
@@ -901,7 +912,7 @@ export const contactsToExportJson = (contacts: Contact[]): DatabaseExport => {
|
||||
did: contact.did,
|
||||
name: contact.name || null,
|
||||
contactMethods: contact.contactMethods
|
||||
? JSON.stringify(contact.contactMethods)
|
||||
? JSON.stringify(parseJsonField(contact.contactMethods, []))
|
||||
: null,
|
||||
nextPubKeyHashB64: contact.nextPubKeyHashB64 || null,
|
||||
notes: contact.notes || null,
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
*
|
||||
* Supported Routes:
|
||||
* - user-profile: View user profile
|
||||
* - project-details: View project details
|
||||
* - project: View project details
|
||||
* - onboard-meeting-setup: Setup onboarding meeting
|
||||
* - invite-one-accept: Accept invitation
|
||||
* - contact-import: Import contacts
|
||||
@@ -81,18 +81,16 @@ export class DeepLinkHandler {
|
||||
string,
|
||||
{ name: string; paramKey?: string }
|
||||
> = {
|
||||
"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": { name: "claim" },
|
||||
"claim-add-raw": { name: "claim-add-raw" },
|
||||
"contact-edit": { name: "contact-edit", paramKey: "did" },
|
||||
contacts: { name: "contacts" },
|
||||
did: { name: "did", paramKey: "did" },
|
||||
"claim-cert": { name: "claim-cert" },
|
||||
"confirm-gift": { name: "confirm-gift" },
|
||||
"did": { name: "did", paramKey: "did" },
|
||||
"invite-one-accept": { name: "invite-one-accept" },
|
||||
"onboard-meeting-members": { name: "onboard-meeting-members" },
|
||||
"onboard-meeting-setup": { name: "onboard-meeting-setup" },
|
||||
"project": { name: "project" },
|
||||
"user-profile": { name: "user-profile" },
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
CameraSource,
|
||||
CameraDirection,
|
||||
} from "@capacitor/camera";
|
||||
import { Capacitor } from "@capacitor/core";
|
||||
import { Share } from "@capacitor/share";
|
||||
import {
|
||||
SQLiteConnection,
|
||||
@@ -247,7 +248,7 @@ export class CapacitorPlatformService implements PlatformService {
|
||||
hasFileSystem: true,
|
||||
hasCamera: true,
|
||||
isMobile: true,
|
||||
isIOS: /iPad|iPhone|iPod/.test(navigator.userAgent),
|
||||
isIOS: Capacitor.getPlatform() === "ios",
|
||||
hasFileDownload: false,
|
||||
needsFileHandlingInstructions: true,
|
||||
isNativeApp: true,
|
||||
|
||||
@@ -211,7 +211,7 @@
|
||||
@click="handleQRCodeClick"
|
||||
>
|
||||
Share Your Info
|
||||
</button>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section
|
||||
@@ -1015,7 +1015,6 @@ import {
|
||||
retrieveSettingsForActiveAccount,
|
||||
updateAccountSettings,
|
||||
} from "../db/index";
|
||||
import { Account } from "../db/tables/accounts";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import {
|
||||
DEFAULT_PASSKEY_EXPIRATION_MINUTES,
|
||||
@@ -1040,7 +1039,6 @@ import {
|
||||
} from "../libs/util";
|
||||
import { UserProfile } from "@/libs/partnerServer";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
|
||||
const inputImportFileNameRef = ref<Blob>();
|
||||
|
||||
@@ -1174,8 +1172,6 @@ export default class AccountViewView extends Vue {
|
||||
5000,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
this.loadingProfile = false;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -1198,6 +1194,8 @@ export default class AccountViewView extends Vue {
|
||||
},
|
||||
5000,
|
||||
);
|
||||
} finally {
|
||||
this.loadingProfile = false;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -1240,7 +1238,6 @@ export default class AccountViewView extends Vue {
|
||||
*/
|
||||
async initializeState() {
|
||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
console.log("settings", settings);
|
||||
if (USE_DEXIE_DB) {
|
||||
await db.open();
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
@@ -1817,7 +1814,7 @@ export default class AccountViewView extends Vue {
|
||||
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
|
||||
try {
|
||||
await databaseUtil.updateAccountSettings(did, {
|
||||
await databaseUtil.updateDidSpecificSettings(did, {
|
||||
isRegistered: true,
|
||||
});
|
||||
if (USE_DEXIE_DB) {
|
||||
@@ -2021,7 +2018,7 @@ export default class AccountViewView extends Vue {
|
||||
if ((error as any).response.status === 404) {
|
||||
logger.error("The image was already deleted:", error);
|
||||
|
||||
await databaseUtil.updateAccountSettings(this.activeDid, {
|
||||
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
|
||||
profileImageUrl: undefined,
|
||||
});
|
||||
if (USE_DEXIE_DB) {
|
||||
|
||||
@@ -198,7 +198,7 @@ export default class ClaimAddRawView extends Vue {
|
||||
this.apiServer,
|
||||
this.axios,
|
||||
);
|
||||
if (result.type === "success") {
|
||||
if (result.success) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
</h2>
|
||||
<div class="flex justify-center w-full">
|
||||
<router-link
|
||||
v-if="veriClaim.id"
|
||||
:to="'/claim-cert/' + encodeURIComponent(veriClaim.id)"
|
||||
class="text-blue-500 mt-2"
|
||||
title="Printable Certificate"
|
||||
@@ -292,15 +293,17 @@
|
||||
<div class="text-sm">
|
||||
{{ didInfo(confirmerId) }}
|
||||
<span v-if="!serverUtil.isEmptyOrHiddenDid(confirmerId)">
|
||||
<a
|
||||
:href="`/did/${confirmerId}`"
|
||||
<router-link
|
||||
:to="{
|
||||
path: '/did/' + encodeURIComponent(confirmerId),
|
||||
}"
|
||||
class="text-blue-500"
|
||||
>
|
||||
<font-awesome
|
||||
icon="arrow-up-right-from-square"
|
||||
class="fa-fw"
|
||||
/>
|
||||
</a>
|
||||
</router-link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -332,15 +335,17 @@
|
||||
<div class="text-sm">
|
||||
{{ didInfo(confsVisibleTo) }}
|
||||
<span v-if="!serverUtil.isEmptyOrHiddenDid(confsVisibleTo)">
|
||||
<a
|
||||
:href="`/did/${confsVisibleTo}`"
|
||||
<router-link
|
||||
:to="{
|
||||
path: '/did/' + encodeURIComponent(confsVisibleTo),
|
||||
}"
|
||||
class="text-blue-500"
|
||||
>
|
||||
<font-awesome
|
||||
icon="arrow-up-right-from-square"
|
||||
class="fa-fw"
|
||||
/>
|
||||
</a>
|
||||
</router-link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -449,15 +454,17 @@
|
||||
<span>
|
||||
{{ didInfo(visDid) }}
|
||||
<span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)">
|
||||
<a
|
||||
:href="`/did/${visDid}`"
|
||||
<router-link
|
||||
:to="{
|
||||
path: '/did/' + encodeURIComponent(visDid),
|
||||
}"
|
||||
class="text-blue-500"
|
||||
>
|
||||
<font-awesome
|
||||
icon="arrow-up-right-from-square"
|
||||
class="fa-fw"
|
||||
/>
|
||||
</a>
|
||||
</router-link>
|
||||
</span>
|
||||
<span v-if="veriClaim.publicUrls?.[visDid]"
|
||||
>, found at <a
|
||||
@@ -934,7 +941,7 @@ export default class ClaimView extends Vue {
|
||||
this.apiServer,
|
||||
this.axios,
|
||||
);
|
||||
if (result.type === "success") {
|
||||
if (result.success) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
|
||||
@@ -407,14 +407,14 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="mt-2 ml-2">
|
||||
<a
|
||||
<router-link
|
||||
v-if="isRegistered"
|
||||
class="text-blue-500 cursor-pointer"
|
||||
:href="urlForNewGive"
|
||||
:to="urlForNewGive"
|
||||
>
|
||||
<font-awesome icon="file-lines" />
|
||||
Record a Give Similar to the Original
|
||||
</a>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -831,7 +831,7 @@ export default class ConfirmGiftView extends Vue {
|
||||
this.apiServer,
|
||||
this.axios,
|
||||
);
|
||||
if (result.type === "success") {
|
||||
if (result.success) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
|
||||
@@ -138,11 +138,13 @@ import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||
|
||||
import QuickNav from "../components/QuickNav.vue";
|
||||
import TopMessage from "../components/TopMessage.vue";
|
||||
import { AppString, NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||
import { db } from "../db/index";
|
||||
import { Contact, ContactMethod } from "../db/tables/contacts";
|
||||
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
import { parseJsonField } from "../db/databaseUtil";
|
||||
import { db } from "../db/index";
|
||||
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
||||
import { Contact, ContactMethod } from "../db/tables/contacts";
|
||||
import { AppString } from "../constants/app";
|
||||
|
||||
/**
|
||||
* Contact Edit View Component
|
||||
@@ -230,9 +232,7 @@ export default class ContactEditView extends Vue {
|
||||
let contact: Contact | undefined = databaseUtil.mapQueryResultToValues(
|
||||
dbContact,
|
||||
)[0] as unknown as Contact;
|
||||
contact.contactMethods = JSON.parse(
|
||||
(contact?.contactMethods as unknown as string) || "[]",
|
||||
);
|
||||
contact.contactMethods = parseJsonField(contact?.contactMethods, []);
|
||||
if (USE_DEXIE_DB) {
|
||||
await db.open();
|
||||
contact = await db.contacts.get(contactDid || "");
|
||||
|
||||
@@ -213,6 +213,7 @@ import {
|
||||
} from "../db/index";
|
||||
import { Contact, ContactMethod } from "../db/tables/contacts";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
import { parseJsonField } from "../db/databaseUtil";
|
||||
import * as libsUtil from "../libs/util";
|
||||
import {
|
||||
capitalizeAndInsertSpacesBeforeCaps,
|
||||
@@ -289,7 +290,7 @@ function dbRecordToContact(record: ContactDbRecord): Contact {
|
||||
profileImageUrl: safeString(record.profileImageUrl),
|
||||
publicKeyBase64: safeString(record.publicKeyBase64),
|
||||
nextPubKeyHashB64: safeString(record.nextPubKeyHashB64),
|
||||
contactMethods: JSON.parse(record.contactMethods || "[]"),
|
||||
contactMethods: parseJsonField(record.contactMethods, []),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -124,6 +124,7 @@ import UserNameDialog from "../components/UserNameDialog.vue";
|
||||
import { generateEndorserJwtUrlForAccount } from "../libs/endorserServer";
|
||||
import { retrieveAccountMetadata } from "../libs/util";
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
import { parseJsonField } from "../db/databaseUtil";
|
||||
|
||||
interface QRScanResult {
|
||||
rawValue?: string;
|
||||
@@ -474,7 +475,9 @@ export default class ContactQRScan extends Vue {
|
||||
|
||||
// Add new contact
|
||||
// @ts-expect-error because we're just using the value to store to the DB
|
||||
contact.contactMethods = JSON.stringify(contact.contactMethods);
|
||||
contact.contactMethods = JSON.stringify(
|
||||
parseJsonField(contact.contactMethods, []),
|
||||
);
|
||||
const { sql, params } = databaseUtil.generateInsertStatement(
|
||||
contact as unknown as Record<string, unknown>,
|
||||
"contacts",
|
||||
|
||||
@@ -171,6 +171,7 @@ import { db, retrieveSettingsForActiveAccount } from "../db/index";
|
||||
import { Contact } from "../db/tables/contacts";
|
||||
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
import { parseJsonField } from "../db/databaseUtil";
|
||||
import { getContactJwtFromJwtUrl } from "../libs/crypto";
|
||||
import {
|
||||
generateEndorserJwtUrlForAccount,
|
||||
@@ -778,7 +779,9 @@ export default class ContactQRScanShow extends Vue {
|
||||
|
||||
// Add new contact
|
||||
// @ts-expect-error because we're just using the value to store to the DB
|
||||
contact.contactMethods = JSON.stringify(contact.contactMethods);
|
||||
contact.contactMethods = JSON.stringify(
|
||||
parseJsonField(contact.contactMethods, []),
|
||||
);
|
||||
const { sql, params } = databaseUtil.generateInsertStatement(
|
||||
contact as unknown as Record<string, unknown>,
|
||||
"contacts",
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2 h-10"
|
||||
/>
|
||||
<button
|
||||
class="px-4 rounded-r bg-green-200 border border-l-0 border-green-400"
|
||||
class="px-4 rounded-r bg-green-200 border border-green-400"
|
||||
@click="onClickNewContact()"
|
||||
>
|
||||
<font-awesome icon="plus" class="fa-fw" />
|
||||
@@ -86,8 +86,8 @@
|
||||
</div>
|
||||
|
||||
<div v-if="contacts.length > 0" class="flex justify-between">
|
||||
<div class="w-full text-left">
|
||||
<div v-if="!showGiveNumbers">
|
||||
<div class="">
|
||||
<div v-if="!showGiveNumbers" class="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="contactsSelected.length === contacts.length"
|
||||
@@ -101,52 +101,33 @@
|
||||
/>
|
||||
<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 ml-3 px-3 py-1.5 rounded-md"
|
||||
:style="
|
||||
:class="
|
||||
contactsSelected.length > 0
|
||||
? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);'
|
||||
: 'background-image: linear-gradient(to bottom, #94a3b8, #374151);'
|
||||
? 'text-md bg-gradient-to-b from-blue-400 to-blue-700 ' +
|
||||
'shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ' +
|
||||
'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"
|
||||
@click="copySelectedContacts()"
|
||||
>
|
||||
Copy Selections
|
||||
</button>
|
||||
<button @click="showCopySelectionsInfo()">
|
||||
<font-awesome
|
||||
icon="circle-info"
|
||||
class="text-xl text-blue-500 ml-4"
|
||||
/>
|
||||
Copy
|
||||
</button>
|
||||
<font-awesome
|
||||
icon="circle-info"
|
||||
class="text-2xl text-blue-500 ml-2"
|
||||
@click="showCopySelectionsInfo()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full text-right">
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
v-if="showGiveNumbers"
|
||||
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 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="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="showGiveAmountsClassNames()"
|
||||
@click="toggleShowGiveTotals()"
|
||||
>
|
||||
@@ -159,6 +140,25 @@
|
||||
}}
|
||||
<font-awesome icon="left-right" class="fa-fw" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
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 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>
|
||||
|
||||
@@ -166,7 +166,7 @@
|
||||
<ul
|
||||
v-if="contacts.length > 0"
|
||||
id="listContacts"
|
||||
class="border-t border-slate-300 mt-1"
|
||||
class="border-t border-slate-300 my-2"
|
||||
>
|
||||
<li
|
||||
v-for="contact in filteredContacts()"
|
||||
@@ -174,125 +174,125 @@
|
||||
class="border-b border-slate-300 pt-1 pb-1"
|
||||
data-testId="contactListItem"
|
||||
>
|
||||
<div class="grow overflow-hidden">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<input
|
||||
v-if="!showGiveNumbers"
|
||||
type="checkbox"
|
||||
:checked="contactsSelected.includes(contact.did)"
|
||||
class="ml-2 h-6 w-6 flex-shrink-0"
|
||||
data-testId="contactCheckOne"
|
||||
@click="
|
||||
contactsSelected.includes(contact.did)
|
||||
? contactsSelected.splice(
|
||||
contactsSelected.indexOf(contact.did),
|
||||
1,
|
||||
)
|
||||
: contactsSelected.push(contact.did)
|
||||
"
|
||||
/>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex overflow-hidden min-w-0 items-center gap-3">
|
||||
<input
|
||||
v-if="!showGiveNumbers"
|
||||
type="checkbox"
|
||||
:checked="contactsSelected.includes(contact.did)"
|
||||
class="ml-2 h-6 w-6 flex-shrink-0"
|
||||
data-testId="contactCheckOne"
|
||||
@click="
|
||||
contactsSelected.includes(contact.did)
|
||||
? contactsSelected.splice(
|
||||
contactsSelected.indexOf(contact.did),
|
||||
1,
|
||||
)
|
||||
: contactsSelected.push(contact.did)
|
||||
"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="flex-shrink-0 w-12 h-12 flex items-center justify-center"
|
||||
>
|
||||
<EntityIcon
|
||||
:contact="contact"
|
||||
:icon-size="48"
|
||||
class="inline-block align-text-bottom border border-slate-300 rounded cursor-pointer overflow-hidden"
|
||||
@click="showLargeIdenticon = contact"
|
||||
/>
|
||||
</div>
|
||||
<EntityIcon
|
||||
:contact="contact"
|
||||
:icon-size="48"
|
||||
class="shrink-0 align-text-bottom border border-slate-300 rounded cursor-pointer overflow-hidden"
|
||||
@click="showLargeIdenticon = contact"
|
||||
/>
|
||||
|
||||
<h2 class="text-base font-semibold w-1/3 truncate flex-shrink-0">
|
||||
{{ contactNameNonBreakingSpace(contact.name) }}
|
||||
<div class="overflow-hidden">
|
||||
<h2 class="text-base font-semibold truncate">
|
||||
<router-link
|
||||
:to="{
|
||||
path: '/did/' + encodeURIComponent(contact.did),
|
||||
}"
|
||||
title="See more about this person"
|
||||
>
|
||||
{{ contactNameNonBreakingSpace(contact.name) }}
|
||||
</router-link>
|
||||
</h2>
|
||||
|
||||
<span>
|
||||
<div class="flex gap-2 items-center">
|
||||
<router-link
|
||||
:to="{
|
||||
path: '/did/' + encodeURIComponent(contact.did),
|
||||
}"
|
||||
title="See more about this person"
|
||||
>
|
||||
<font-awesome
|
||||
icon="circle-info"
|
||||
class="text-xl text-blue-500"
|
||||
/>
|
||||
</router-link>
|
||||
<div class="flex gap-1.5 items-center overflow-hidden">
|
||||
<router-link
|
||||
:to="{
|
||||
path: '/did/' + encodeURIComponent(contact.did),
|
||||
}"
|
||||
title="See more about this person"
|
||||
>
|
||||
<font-awesome
|
||||
icon="circle-info"
|
||||
class="text-base text-blue-500"
|
||||
/>
|
||||
</router-link>
|
||||
|
||||
<span class="text-sm overflow-hidden">{{
|
||||
libsUtil.shortDid(contact.did)
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
{{ contact.notes }}
|
||||
</div>
|
||||
</span>
|
||||
<span class="text-xs truncate">{{ contact.did }}</span>
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
{{ contact.notes }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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
|
||||
v-if="showGiveNumbers && contact.did != activeDid"
|
||||
class="flex gap-2 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 py-1.5 rounded-md"
|
||||
data-testId="offerButton"
|
||||
@click="openOfferDialog(contact.did, contact.name)"
|
||||
>
|
||||
<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>
|
||||
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 -ml-1.5 px-2 py-1.5 rounded-r-md border-l"
|
||||
:title="givenByMeDescriptions[contact.did] || ''"
|
||||
@click="confirmShowGiftedDialog(activeDid, contact.did)"
|
||||
>
|
||||
To:
|
||||
<br />
|
||||
{{
|
||||
/* 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>
|
||||
|
||||
<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>
|
||||
<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"
|
||||
title="See more given activity"
|
||||
>
|
||||
<font-awesome icon="file-lines" class="fa-fw" />
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
@@ -314,16 +314,18 @@
|
||||
/>
|
||||
<button
|
||||
v-if="!showGiveNumbers"
|
||||
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="
|
||||
:class="
|
||||
contactsSelected.length > 0
|
||||
? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);'
|
||||
: 'background-image: linear-gradient(to bottom, #94a3b8, #374151);'
|
||||
? 'text-md bg-gradient-to-b from-blue-400 to-blue-700 ' +
|
||||
'shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ' +
|
||||
'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()"
|
||||
>
|
||||
Copy Selections
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -542,7 +544,7 @@ export default class ContactsView extends Vue {
|
||||
if (response.status != 201) {
|
||||
throw { error: { response: response } };
|
||||
}
|
||||
await databaseUtil.updateAccountSettings(this.activeDid, {
|
||||
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
|
||||
isRegistered: true,
|
||||
});
|
||||
if (USE_DEXIE_DB) {
|
||||
@@ -998,8 +1000,6 @@ export default class ContactsView extends Vue {
|
||||
newContact as unknown as Record<string, unknown>,
|
||||
"contacts",
|
||||
);
|
||||
logger.error("sql", sql);
|
||||
logger.error("params", params);
|
||||
let contactPromise = platformService.dbExec(sql, params);
|
||||
if (USE_DEXIE_DB) {
|
||||
// @ts-expect-error since the result of this promise won't be used, and this will go away soon
|
||||
|
||||
@@ -788,7 +788,7 @@ export default class DiscoverView extends Vue {
|
||||
const route = {
|
||||
path: this.isProjectsActive
|
||||
? "/project/" + encodeURIComponent(id)
|
||||
: "/userProfile/" + encodeURIComponent(id),
|
||||
: "/user-profile/" + encodeURIComponent(id),
|
||||
};
|
||||
this.$router.push(route);
|
||||
}
|
||||
|
||||
@@ -825,11 +825,8 @@ export default class GiftedDetails extends Vue {
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
result.type === "error" ||
|
||||
this.isGiveCreationError(result.response)
|
||||
) {
|
||||
const errorMessage = this.getGiveCreationErrorMessage(result);
|
||||
if (!result.success) {
|
||||
const errorMessage = result.error;
|
||||
logger.error("Error with give creation result:", result);
|
||||
this.$notify(
|
||||
{
|
||||
@@ -902,28 +899,6 @@ export default class GiftedDetails extends Vue {
|
||||
|
||||
// Helper functions for readability
|
||||
|
||||
/**
|
||||
* @param result response "data" from the server
|
||||
* @returns true if the result indicates an error
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
isGiveCreationError(result: any) {
|
||||
return result.status !== 201 || result.data?.error;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
|
||||
* @returns best guess at an error message
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getGiveCreationErrorMessage(result: any) {
|
||||
return (
|
||||
result.error?.userMessage ||
|
||||
result.error?.error ||
|
||||
result.response?.data?.error?.message
|
||||
);
|
||||
}
|
||||
|
||||
explainData() {
|
||||
this.$notify(
|
||||
{
|
||||
|
||||
@@ -24,11 +24,11 @@
|
||||
<!-- eslint-disable prettier/prettier max-len -->
|
||||
<div>
|
||||
<p>
|
||||
This app focuses on gifts & gratitude, using them to build cool things together with your network.
|
||||
This app focuses on raw gratitude, using it to build cool things together with your network.
|
||||
</p>
|
||||
|
||||
<p class="ml-4">
|
||||
If you'd like to see the page-by-page help,
|
||||
If you'd like to see the page-by-page help again,
|
||||
<span
|
||||
class="text-blue-500 cursor-pointer"
|
||||
@click="unsetFinishedOnboarding()"
|
||||
@@ -555,9 +555,6 @@
|
||||
initiative.
|
||||
</p>
|
||||
|
||||
<h2 class="text-xl font-semibold">What app version is this?</h2>
|
||||
<p>{{ package.version }} ({{ commitHash }})</p>
|
||||
|
||||
<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.
|
||||
</h2>
|
||||
@@ -567,6 +564,28 @@
|
||||
>info@TimeSafari.app</a
|
||||
>
|
||||
</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>
|
||||
<!-- eslint enable -->
|
||||
</section>
|
||||
@@ -603,6 +622,7 @@ export default class HelpView extends Vue {
|
||||
showVerifiable = false;
|
||||
|
||||
APP_SERVER = APP_SERVER;
|
||||
Capacitor = Capacitor;
|
||||
|
||||
// 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.
|
||||
@@ -622,7 +642,7 @@ export default class HelpView extends Vue {
|
||||
}
|
||||
|
||||
if (settings.activeDid) {
|
||||
await databaseUtil.updateAccountSettings(settings.activeDid, {
|
||||
await databaseUtil.updateDidSpecificSettings(settings.activeDid, {
|
||||
finishedOnboarding: false,
|
||||
});
|
||||
if (USE_DEXIE_DB) {
|
||||
|
||||
@@ -630,7 +630,7 @@ export default class HomeView extends Vue {
|
||||
this.activeDid,
|
||||
);
|
||||
if (resp.status === 200) {
|
||||
await databaseUtil.updateAccountSettings(this.activeDid, {
|
||||
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
|
||||
isRegistered: true,
|
||||
...(await databaseUtil.retrieveSettingsForActiveAccount()),
|
||||
});
|
||||
@@ -785,7 +785,7 @@ export default class HomeView extends Vue {
|
||||
if (USE_DEXIE_DB) {
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
}
|
||||
await databaseUtil.updateAccountSettings(this.activeDid, {
|
||||
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
|
||||
apiServer: this.apiServer,
|
||||
isRegistered: true,
|
||||
...settings,
|
||||
@@ -1843,7 +1843,7 @@ export default class HomeView extends Vue {
|
||||
this.axios,
|
||||
);
|
||||
|
||||
if (result.type === "success") {
|
||||
if (result.success) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
|
||||
@@ -79,9 +79,14 @@ import {
|
||||
newIdentifier,
|
||||
nextDerivationPath,
|
||||
} from "../libs/crypto";
|
||||
import { accountsDBPromise, db } from "../db/index";
|
||||
import * as databaseUtil from "../db/databaseUtil";
|
||||
import { db } from "../db/index";
|
||||
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
|
||||
import { retrieveAllAccountsMetadata, retrieveFullyDecryptedAccount, saveNewIdentity } from "../libs/util";
|
||||
import {
|
||||
retrieveAllAccountsMetadata,
|
||||
retrieveFullyDecryptedAccount,
|
||||
saveNewIdentity,
|
||||
} from "../libs/util";
|
||||
import { logger } from "../utils/logger";
|
||||
import { Account, AccountEncrypted } from "../db/tables/accounts";
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
@@ -100,13 +105,20 @@ export default class ImportAccountView extends Vue {
|
||||
|
||||
async mounted() {
|
||||
const accounts: AccountEncrypted[] = await retrieveAllAccountsMetadata();
|
||||
const decryptedAccounts: (Account | undefined)[] = await Promise.all(accounts.map(async (account) => {
|
||||
return retrieveFullyDecryptedAccount(account.did);
|
||||
}));
|
||||
const filteredDecryptedAccounts: Account[] = decryptedAccounts.filter((account) => account !== undefined);
|
||||
const decryptedAccounts: (Account | undefined)[] = await Promise.all(
|
||||
accounts.map(async (account) => {
|
||||
return retrieveFullyDecryptedAccount(account.did);
|
||||
}),
|
||||
);
|
||||
const filteredDecryptedAccounts: Account[] = decryptedAccounts.filter(
|
||||
(account) => account !== undefined,
|
||||
);
|
||||
|
||||
// group by account.mnemonic
|
||||
const groupedAccounts: Record<string, Account[]> = R.groupBy((a) => a.mnemonic || "", filteredDecryptedAccounts) as Record<string, Account[]>;
|
||||
const groupedAccounts: Record<string, Account[]> = R.groupBy(
|
||||
(a) => a.mnemonic || "",
|
||||
filteredDecryptedAccounts,
|
||||
) as Record<string, Account[]>;
|
||||
|
||||
this.didArrays = groupedAccounts;
|
||||
if (Object.keys(this.didArrays).length > 0) {
|
||||
@@ -125,10 +137,13 @@ export default class ImportAccountView extends Vue {
|
||||
public async incrementDerivation() {
|
||||
// find the maximum derivation path for the selected DIDs
|
||||
const selectedArray: Array<Account> =
|
||||
Object.values(this.didArrays).find((dids) => dids[0].did === this.selectedArrayFirstDid) ||
|
||||
[];
|
||||
Object.values(this.didArrays).find(
|
||||
(dids) => dids[0].did === this.selectedArrayFirstDid,
|
||||
) || [];
|
||||
// extract the derivationPath array and sort it
|
||||
const derivationPaths = selectedArray.map((account) => account.derivationPath);
|
||||
const derivationPaths = selectedArray.map(
|
||||
(account) => account.derivationPath,
|
||||
);
|
||||
derivationPaths.sort((a, b) => {
|
||||
const aParts = a?.split("/");
|
||||
const aLast = aParts?.[aParts.length - 1];
|
||||
@@ -137,7 +152,9 @@ export default class ImportAccountView extends Vue {
|
||||
return parseInt(aLast || "0") - parseInt(bLast || "0");
|
||||
});
|
||||
// we're sure there's at least one
|
||||
const maxDerivPath: string = derivationPaths[derivationPaths.length - 1] as string;
|
||||
const maxDerivPath: string = derivationPaths[
|
||||
derivationPaths.length - 1
|
||||
] as string;
|
||||
|
||||
const newDerivPath = nextDerivationPath(maxDerivPath);
|
||||
|
||||
@@ -148,23 +165,15 @@ export default class ImportAccountView extends Vue {
|
||||
|
||||
try {
|
||||
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
|
||||
const platformService = PlatformServiceFactory.getInstance();
|
||||
await platformService.dbExec("UPDATE settings SET activeDid = ?", [
|
||||
newId.did,
|
||||
]);
|
||||
await databaseUtil.updateDidSpecificSettings(newId.did, {
|
||||
isRegistered: false,
|
||||
});
|
||||
if (USE_DEXIE_DB) {
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
activeDid: newId.did,
|
||||
|
||||
@@ -257,7 +257,7 @@ export default class NewActivityView extends Vue {
|
||||
async expandOffersToUserAndMarkRead() {
|
||||
this.showOffersDetails = !this.showOffersDetails;
|
||||
if (this.showOffersDetails) {
|
||||
await databaseUtil.updateAccountSettings(this.activeDid, {
|
||||
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
|
||||
lastAckedOfferToUserJwtId: this.newOffersToUser[0].jwtId,
|
||||
});
|
||||
if (USE_DEXIE_DB) {
|
||||
@@ -285,7 +285,7 @@ export default class NewActivityView extends Vue {
|
||||
);
|
||||
if (index !== -1 && index < this.newOffersToUser.length - 1) {
|
||||
// Set to the next offer's jwtId
|
||||
await databaseUtil.updateAccountSettings(this.activeDid, {
|
||||
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
|
||||
lastAckedOfferToUserJwtId: this.newOffersToUser[index + 1].jwtId,
|
||||
});
|
||||
if (USE_DEXIE_DB) {
|
||||
@@ -295,7 +295,7 @@ export default class NewActivityView extends Vue {
|
||||
}
|
||||
} else {
|
||||
// it's the last entry (or not found), so just keep it the same
|
||||
await databaseUtil.updateAccountSettings(this.activeDid, {
|
||||
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
|
||||
lastAckedOfferToUserJwtId: this.lastAckedOfferToUserJwtId,
|
||||
});
|
||||
if (USE_DEXIE_DB) {
|
||||
@@ -319,7 +319,7 @@ export default class NewActivityView extends Vue {
|
||||
this.showOffersToUserProjectsDetails =
|
||||
!this.showOffersToUserProjectsDetails;
|
||||
if (this.showOffersToUserProjectsDetails) {
|
||||
await databaseUtil.updateAccountSettings(this.activeDid, {
|
||||
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
|
||||
lastAckedOfferToUserProjectsJwtId:
|
||||
this.newOffersToUserProjects[0].jwtId,
|
||||
});
|
||||
@@ -349,7 +349,7 @@ export default class NewActivityView extends Vue {
|
||||
);
|
||||
if (index !== -1 && index < this.newOffersToUserProjects.length - 1) {
|
||||
// Set to the next offer's jwtId
|
||||
await databaseUtil.updateAccountSettings(this.activeDid, {
|
||||
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
|
||||
lastAckedOfferToUserProjectsJwtId:
|
||||
this.newOffersToUserProjects[index + 1].jwtId,
|
||||
});
|
||||
@@ -361,7 +361,7 @@ export default class NewActivityView extends Vue {
|
||||
}
|
||||
} else {
|
||||
// it's the last entry (or not found), so just keep it the same
|
||||
await databaseUtil.updateAccountSettings(this.activeDid, {
|
||||
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
|
||||
lastAckedOfferToUserProjectsJwtId:
|
||||
this.lastAckedOfferToUserProjectsJwtId,
|
||||
});
|
||||
|
||||
@@ -52,19 +52,24 @@
|
||||
icon="user"
|
||||
class="fa-fw text-slate-400"
|
||||
></font-awesome>
|
||||
{{ issuerInfoObject?.displayName }}
|
||||
<span v-if="!serverUtil.isEmptyOrHiddenDid(issuer)">
|
||||
<a
|
||||
:href="`/did/${issuer}`"
|
||||
class="text-blue-500"
|
||||
<span class="truncate inline-block max-w-[calc(100%-2rem)]">
|
||||
{{ issuerInfoObject?.displayName }}
|
||||
</span>
|
||||
<span 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
|
||||
icon="arrow-up-right-from-square"
|
||||
class="fa-fw"
|
||||
/>
|
||||
</a>
|
||||
</router-link>
|
||||
</span>
|
||||
<span v-else-if="serverUtil.isHiddenDid(issuer)">
|
||||
<span v-if="serverUtil.isHiddenDid(issuer)" class="ml-1">
|
||||
<font-awesome
|
||||
icon="info-circle"
|
||||
class="fa-fw text-blue-500 cursor-pointer"
|
||||
@@ -1428,7 +1433,7 @@ export default class ProjectViewView extends Vue {
|
||||
this.apiServer,
|
||||
this.axios,
|
||||
);
|
||||
if (result.type === "success") {
|
||||
if (result.success) {
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
|
||||
@@ -155,7 +155,7 @@ import { Contact } from "../db/tables/contacts";
|
||||
import {
|
||||
GenericCredWrapper,
|
||||
GenericVerifiableCredential,
|
||||
ErrorResult,
|
||||
CreateAndSubmitClaimResult,
|
||||
} from "../interfaces";
|
||||
import {
|
||||
BVC_MEETUPS_PROJECT_CLAIM_ID,
|
||||
@@ -230,7 +230,9 @@ export default class QuickActionBvcBeginView extends Vue {
|
||||
suppressMilliseconds: true,
|
||||
}) || "";
|
||||
|
||||
this.allMyDids = (await retrieveAllAccountsMetadata()).map((account) => account.did);
|
||||
this.allMyDids = (await retrieveAllAccountsMetadata()).map(
|
||||
(account) => account.did,
|
||||
);
|
||||
if (USE_DEXIE_DB) {
|
||||
const accountsDB = await accountsDBPromise;
|
||||
await accountsDB.open();
|
||||
@@ -296,13 +298,13 @@ export default class QuickActionBvcBeginView extends Vue {
|
||||
}
|
||||
|
||||
// in parallel, make a confirmation for each selected claim and send them all to the server
|
||||
const confirmResults = await Promise.allSettled(
|
||||
const confirmResults: PromiseSettledResult<CreateAndSubmitClaimResult>[] = await Promise.allSettled(
|
||||
this.claimsToConfirmSelected.map(async (jwtId) => {
|
||||
const record = this.claimsToConfirm.find(
|
||||
(claim) => claim.id === jwtId,
|
||||
);
|
||||
if (!record) {
|
||||
return { type: "error", error: "Record not found." };
|
||||
return { success: false, error: "Record not found." };
|
||||
}
|
||||
return createAndSubmitConfirmation(
|
||||
this.activeDid,
|
||||
@@ -316,8 +318,8 @@ export default class QuickActionBvcBeginView extends Vue {
|
||||
);
|
||||
// check for any rejected confirmations
|
||||
const confirmsSucceeded = confirmResults.filter(
|
||||
(result) =>
|
||||
result.status === "fulfilled" && result.value.type === "success",
|
||||
// 'fulfilled' is the status in a successful PromiseFulfilledResult
|
||||
(result) => result.status === "fulfilled" && result.value.success,
|
||||
);
|
||||
if (confirmsSucceeded.length < this.claimsToConfirmSelected.length) {
|
||||
logger.error("Error sending confirmations:", confirmResults);
|
||||
@@ -351,7 +353,7 @@ export default class QuickActionBvcBeginView extends Vue {
|
||||
undefined,
|
||||
BVC_MEETUPS_PROJECT_CLAIM_ID,
|
||||
);
|
||||
giveSucceeded = giveResult.type === "success";
|
||||
giveSucceeded = giveResult.success;
|
||||
if (!giveSucceeded) {
|
||||
logger.error("Error sending give:", giveResult);
|
||||
this.$notify(
|
||||
@@ -360,7 +362,7 @@ export default class QuickActionBvcBeginView extends Vue {
|
||||
type: "danger",
|
||||
title: "Error",
|
||||
text:
|
||||
(giveResult as ErrorResult)?.error?.userMessage ||
|
||||
(giveResult as CreateAndSubmitClaimResult)?.error ||
|
||||
"There was an error sending that give.",
|
||||
},
|
||||
5000,
|
||||
|
||||
@@ -215,7 +215,7 @@ export default class SearchAreaView extends Vue {
|
||||
if (USE_DEXIE_DB) {
|
||||
await db.open();
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
searchBoxes: [newSearchBox],
|
||||
searchBoxes: searchBoxes as unknown, // Type assertion for Dexie compatibility
|
||||
});
|
||||
}
|
||||
this.searchBox = newSearchBox;
|
||||
@@ -269,7 +269,7 @@ export default class SearchAreaView extends Vue {
|
||||
if (USE_DEXIE_DB) {
|
||||
await db.open();
|
||||
await db.settings.update(MASTER_SETTINGS_KEY, {
|
||||
searchBoxes: [],
|
||||
searchBoxes: "[]" as unknown as string, // Type assertion for Dexie compatibility
|
||||
filterFeedByNearby: false,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user