Merge branch 'sql-absurd-sql-back'

This commit is contained in:
2025-06-07 17:18:10 -06:00
147 changed files with 10865 additions and 3762 deletions

View File

@@ -330,8 +330,11 @@
<script lang="ts">
import { Vue, Component } from "vue-facing-decorator";
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from "./db/index";
import { NotificationIface } from "./constants/app";
import { NotificationIface, USE_DEXIE_DB } from "./constants/app";
import * as databaseUtil from "./db/databaseUtil";
import { retrieveSettingsForActiveAccount } from "./db/index";
import { logConsoleAndDb } from "./db/databaseUtil";
import { logger } from "./utils/logger";
interface Settings {
@@ -396,7 +399,11 @@ export default class App extends Vue {
try {
logger.log("Retrieving settings for the active account...");
const settings: Settings = await retrieveSettingsForActiveAccount();
let settings: Settings =
await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
logger.log("Retrieved settings:", settings);
const notifyingNewActivity = !!settings?.notifyingNewActivityTime;

View File

@@ -61,7 +61,7 @@
<!-- Record Image -->
<div
v-if="record.image"
class="bg-cover mb-4 -mt-3 sm:-mt-4 -mx-3 sm:-mx-4"
class="bg-cover mb-2 -mt-3 sm:-mt-4 -mx-3 sm:-mx-4"
:style="`background-image: url(${record.image});`"
>
<a
@@ -77,8 +77,15 @@
</a>
</div>
<!-- Description -->
<p class="font-medium">
<a class="cursor-pointer" @click="$emit('loadClaim', record.jwtId)">
{{ description }}
</a>
</p>
<div
class="relative flex justify-between gap-4 max-w-[40rem] mx-auto mb-3"
class="relative flex justify-between gap-4 max-w-[40rem] mx-auto mt-4"
>
<!-- Source -->
<div
@@ -229,13 +236,6 @@
</div>
</div>
</div>
<!-- Description -->
<p class="font-medium">
<a class="cursor-pointer" @click="$emit('loadClaim', record.jwtId)">
{{ description }}
</a>
</p>
</div>
</li>
</template>
@@ -309,7 +309,7 @@ export default class ActivityListItem extends Vue {
const claim =
(this.record.fullClaim as unknown).claim || this.record.fullClaim;
return `${claim.description}`;
return `${claim?.description || ""}`;
}
private displayAmount(code: string, amt: number) {

View File

@@ -24,9 +24,7 @@ backup and database export, with platform-specific download instructions. * *
class="block w-full text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
@click="exportDatabase()"
>
Download Settings & Contacts
<br />
(excluding Identifier Data)
Download Contacts
</button>
<a
ref="downloadLink"
@@ -62,14 +60,18 @@ backup and database export, with platform-specific download instructions. * *
<script lang="ts">
import { Component, Prop, Vue } from "vue-facing-decorator";
import { NotificationIface } from "../constants/app";
import { db } from "../db/index";
import { AppString, NotificationIface } from "../constants/app";
import { Contact } from "../db/tables/contacts";
import * as databaseUtil from "../db/databaseUtil";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
import {
PlatformService,
PlatformCapabilities,
} from "../services/PlatformService";
import { contactsToExportJson } from "../libs/util";
/**
* @vue-component
@@ -131,21 +133,25 @@ export default class DataExportSection extends Vue {
*/
public async exportDatabase() {
try {
const blob = await db.export({
prettyJson: true,
transform: (table, value, key) => {
if (table === "contacts") {
// Dexie inserts a number 0 when some are undefined, so we need to totally remove them.
Object.keys(value).forEach((prop) => {
if (value[prop] === undefined) {
delete value[prop];
}
});
}
return { value, key };
},
});
const fileName = `${db.name}-backup.json`;
let allContacts: Contact[] = [];
const platformService = PlatformServiceFactory.getInstance();
const result = await platformService.dbQuery(`SELECT * FROM contacts`);
if (result) {
allContacts = databaseUtil.mapQueryResultToValues(
result,
) as unknown as Contact[];
}
// if (USE_DEXIE_DB) {
// await db.open();
// allContacts = await db.contacts.toArray();
// }
// Convert contacts to export format
const exportData = contactsToExportJson(allContacts);
const jsonStr = JSON.stringify(exportData, null, 2);
const blob = new Blob([jsonStr], { type: "application/json" });
const fileName = `${AppString.APP_NAME_NO_SPACES}-backup-contacts.json`;
if (this.platformCapabilities.hasFileDownload) {
// Web platform: Use download link
@@ -157,8 +163,9 @@ export default class DataExportSection extends Vue {
setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000);
} else if (this.platformCapabilities.hasFileSystem) {
// Native platform: Write to app directory
const content = await blob.text();
await this.platformService.writeAndShareFile(fileName, content);
await this.platformService.writeAndShareFile(fileName, jsonStr);
} else {
throw new Error("This platform does not support file downloads.");
}
this.$notify(
@@ -167,10 +174,10 @@ export default class DataExportSection extends Vue {
type: "success",
title: "Export Successful",
text: this.platformCapabilities.hasFileDownload
? "See your downloads directory for the backup. It is in the Dexie format."
: "You should have been prompted to save your backup file.",
? "See your downloads directory for the backup."
: "The backup file has been saved.",
},
-1,
3000,
);
} catch (error) {
logger.error("Export Error:", error);

View File

@@ -99,8 +99,12 @@ import {
LTileLayer,
} from "@vue-leaflet/vue-leaflet";
import { Router } from "vue-router";
import { USE_DEXIE_DB } from "@/constants/app";
import * as databaseUtil from "../db/databaseUtil";
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
@Component({
components: {
@@ -122,7 +126,10 @@ export default class FeedFilters extends Vue {
async open(onCloseIfChanged: () => void) {
this.onCloseIfChanged = onCloseIfChanged;
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.hasVisibleDid = !!settings.filterFeedByVisible;
this.isNearby = !!settings.filterFeedByNearby;
if (settings.searchBoxes && settings.searchBoxes.length > 0) {
@@ -144,9 +151,17 @@ export default class FeedFilters extends Vue {
async toggleNearby() {
this.settingChanged = true;
this.isNearby = !this.isNearby;
await db.settings.update(MASTER_SETTINGS_KEY, {
filterFeedByNearby: this.isNearby,
});
const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec(
`UPDATE settings SET filterFeedByNearby = ? WHERE id = ?`,
[this.isNearby, MASTER_SETTINGS_KEY],
);
if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, {
filterFeedByNearby: this.isNearby,
});
}
}
async clearAll() {
@@ -154,10 +169,18 @@ export default class FeedFilters extends Vue {
this.settingChanged = true;
}
await db.settings.update(MASTER_SETTINGS_KEY, {
filterFeedByNearby: false,
filterFeedByVisible: false,
});
const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec(
`UPDATE settings SET filterFeedByNearby = ? AND filterFeedByVisible = ? WHERE id = ?`,
[false, false, MASTER_SETTINGS_KEY],
);
if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, {
filterFeedByNearby: false,
filterFeedByVisible: false,
});
}
this.hasVisibleDid = false;
this.isNearby = false;
@@ -168,10 +191,18 @@ export default class FeedFilters extends Vue {
this.settingChanged = true;
}
await db.settings.update(MASTER_SETTINGS_KEY, {
filterFeedByNearby: true,
filterFeedByVisible: true,
});
const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec(
`UPDATE settings SET filterFeedByNearby = ? AND filterFeedByVisible = ? WHERE id = ?`,
[true, true, MASTER_SETTINGS_KEY],
);
if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, {
filterFeedByNearby: true,
filterFeedByVisible: true,
});
}
this.hasVisibleDid = true;
this.isNearby = true;

View File

@@ -89,7 +89,7 @@
<script lang="ts">
import { Vue, Component, Prop } from "vue-facing-decorator";
import { NotificationIface } from "../constants/app";
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
import {
createAndSubmitGive,
didInfo,
@@ -98,8 +98,10 @@ import {
import * as libsUtil from "../libs/util";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
import { Contact } from "../db/tables/contacts";
import * as databaseUtil from "../db/databaseUtil";
import { retrieveAccountDids } from "../libs/util";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
@Component
export default class GiftedDialog extends Vue {
@@ -144,11 +146,23 @@ export default class GiftedDialog extends Vue {
this.offerId = offerId || "";
try {
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.apiServer = settings.apiServer || "";
this.activeDid = settings.activeDid || "";
this.allContacts = await db.contacts.toArray();
const platformService = PlatformServiceFactory.getInstance();
const result = await platformService.dbQuery(`SELECT * FROM contacts`);
if (result) {
this.allContacts = databaseUtil.mapQueryResultToValues(
result,
) as unknown as Contact[];
}
if (USE_DEXIE_DB) {
this.allContacts = await db.contacts.toArray();
}
this.allMyDids = await retrieveAccountDids();

View File

@@ -74,10 +74,12 @@
import { Vue, Component } from "vue-facing-decorator";
import { Router } from "vue-router";
import { AppString, NotificationIface } from "../constants/app";
import { AppString, NotificationIface, USE_DEXIE_DB } from "../constants/app";
import { db } from "../db/index";
import { Contact } from "../db/tables/contacts";
import * as databaseUtil from "../db/databaseUtil";
import { GiverReceiverInputInfo } from "../libs/util";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
@Component
export default class GivenPrompts extends Vue {
@@ -127,8 +129,16 @@ export default class GivenPrompts extends Vue {
this.visible = true;
this.callbackOnFullGiftInfo = callbackOnFullGiftInfo;
await db.open();
this.numContacts = await db.contacts.count();
const platformService = PlatformServiceFactory.getInstance();
const result = await platformService.dbQuery(
"SELECT COUNT(*) FROM contacts",
);
if (result) {
this.numContacts = result.values[0][0] as number;
}
if (USE_DEXIE_DB) {
this.numContacts = await db.contacts.count();
}
this.shownContactDbIndices = new Array<boolean>(this.numContacts); // all undefined to start
}
@@ -229,10 +239,22 @@ export default class GivenPrompts extends Vue {
this.nextIdeaPastContacts();
} else {
// get the contact at that offset
await db.open();
this.currentContact = await db.contacts
.offset(someContactDbIndex)
.first();
const platformService = PlatformServiceFactory.getInstance();
const result = await platformService.dbQuery(
"SELECT * FROM contacts LIMIT 1 OFFSET ?",
[someContactDbIndex],
);
if (result) {
this.currentContact = databaseUtil.mapQueryResultToValues(result)[
someContactDbIndex
] as unknown as Contact;
}
if (USE_DEXIE_DB) {
await db.open();
this.currentContact = await db.contacts
.offset(someContactDbIndex)
.first();
}
this.shownContactDbIndices[someContactDbIndex] = true;
}
}

View File

@@ -256,11 +256,16 @@ import axios from "axios";
import { ref } from "vue";
import { Component, Vue } from "vue-facing-decorator";
import VuePictureCropper, { cropper } from "vue-picture-cropper";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
import {
DEFAULT_IMAGE_API_SERVER,
NotificationIface,
USE_DEXIE_DB,
} from "../constants/app";
import { retrieveSettingsForActiveAccount } from "../db/index";
import { accessToken } from "../libs/crypto";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
import * as databaseUtil from "../db/databaseUtil";
const inputImageFileNameRef = ref<Blob>();
@@ -350,9 +355,11 @@ export default class ImageMethodDialog extends Vue {
* @throws {Error} When settings retrieval fails
*/
async mounted() {
logger.log("ImageMethodDialog mounted");
try {
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.activeDid = settings.activeDid || "";
} catch (error: unknown) {
logger.error("Error retrieving settings from database:", error);

View File

@@ -172,8 +172,10 @@ import {
} from "../libs/endorserServer";
import { decryptMessage } from "../libs/crypto";
import { Contact } from "../db/tables/contacts";
import * as databaseUtil from "../db/databaseUtil";
import * as libsUtil from "../libs/util";
import { NotificationIface } from "../constants/app";
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
interface Member {
admitted: boolean;
@@ -209,7 +211,10 @@ export default class MembersList extends Vue {
contacts: Array<Contact> = [];
async created() {
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.firstName = settings.firstName || "";
@@ -355,7 +360,16 @@ export default class MembersList extends Vue {
}
async loadContacts() {
this.contacts = await db.contacts.toArray();
const platformService = PlatformServiceFactory.getInstance();
const result = await platformService.dbQuery("SELECT * FROM contacts");
if (result) {
this.contacts = databaseUtil.mapQueryResultToValues(
result,
) as unknown as Contact[];
}
if (USE_DEXIE_DB) {
this.contacts = await db.contacts.toArray();
}
}
getContactFor(did: string): Contact | undefined {
@@ -439,7 +453,14 @@ export default class MembersList extends Vue {
if (result.success) {
decrMember.isRegistered = true;
if (oldContact) {
await db.contacts.update(decrMember.did, { registered: true });
const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec(
"UPDATE contacts SET registered = ? WHERE did = ?",
[true, decrMember.did],
);
if (USE_DEXIE_DB) {
await db.contacts.update(decrMember.did, { registered: true });
}
oldContact.registered = true;
}
this.$notify(
@@ -492,7 +513,14 @@ export default class MembersList extends Vue {
name: member.name,
};
await db.contacts.add(newContact);
const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec(
"INSERT INTO contacts (did, name) VALUES (?, ?)",
[member.did, member.name],
);
if (USE_DEXIE_DB) {
await db.contacts.add(newContact);
}
this.contacts.push(newContact);
this.$notify(

View File

@@ -82,12 +82,13 @@
<script lang="ts">
import { Vue, Component, Prop } from "vue-facing-decorator";
import { NotificationIface } from "../constants/app";
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
import {
createAndSubmitOffer,
serverMessageForUser,
} from "../libs/endorserServer";
import * as libsUtil from "../libs/util";
import * as databaseUtil from "../db/databaseUtil";
import { retrieveSettingsForActiveAccount } from "../db/index";
import { logger } from "../utils/logger";
@@ -116,7 +117,10 @@ export default class OfferDialog extends Vue {
this.recipientDid = recipientDid;
this.recipientName = recipientName;
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.apiServer = settings.apiServer || "";
this.activeDid = settings.activeDid || "";

View File

@@ -201,13 +201,16 @@
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { NotificationIface } from "../constants/app";
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
import {
db,
retrieveSettingsForActiveAccount,
updateAccountSettings,
} from "../db/index";
import * as databaseUtil from "../db/databaseUtil";
import { OnboardPage } from "../libs/util";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { Contact } from "@/db/tables/contacts";
@Component({
computed: {
@@ -222,7 +225,7 @@ export default class OnboardingDialog extends Vue {
$router!: Router;
activeDid = "";
firstContactName = null;
firstContactName = "";
givenName = "";
isRegistered = false;
numContacts = 0;
@@ -231,29 +234,54 @@ export default class OnboardingDialog extends Vue {
async open(page: OnboardPage) {
this.page = page;
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.activeDid = settings.activeDid || "";
this.isRegistered = !!settings.isRegistered;
const contacts = await db.contacts.toArray();
this.numContacts = contacts.length;
if (this.numContacts > 0) {
this.firstContactName = contacts[0].name;
const platformService = PlatformServiceFactory.getInstance();
const dbContacts = await platformService.dbQuery("SELECT * FROM contacts");
if (dbContacts) {
this.numContacts = dbContacts.values.length;
const firstContact = dbContacts.values[0];
const fullContact = databaseUtil.mapColumnsToValues(dbContacts.columns, [
firstContact,
]) as unknown as Contact;
this.firstContactName = fullContact.name || "";
}
if (USE_DEXIE_DB) {
const contacts = await db.contacts.toArray();
this.numContacts = contacts.length;
if (this.numContacts > 0) {
this.firstContactName = contacts[0].name || "";
}
}
this.visible = true;
if (this.page === OnboardPage.Create) {
// we'll assume that they've been through all the other pages
await updateAccountSettings(this.activeDid, {
await databaseUtil.updateAccountSettings(this.activeDid, {
finishedOnboarding: true,
});
if (USE_DEXIE_DB) {
await updateAccountSettings(this.activeDid, {
finishedOnboarding: true,
});
}
}
}
async onClickClose(done?: boolean, goHome?: boolean) {
this.visible = false;
if (done) {
await updateAccountSettings(this.activeDid, {
await databaseUtil.updateAccountSettings(this.activeDid, {
finishedOnboarding: true,
});
if (USE_DEXIE_DB) {
await updateAccountSettings(this.activeDid, {
finishedOnboarding: true,
});
}
if (goHome) {
this.$router.push({ name: "home" });
}

View File

@@ -119,7 +119,12 @@ PhotoDialog.vue */
import axios from "axios";
import { Component, Vue } from "vue-facing-decorator";
import VuePictureCropper, { cropper } from "vue-picture-cropper";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
import {
DEFAULT_IMAGE_API_SERVER,
NotificationIface,
USE_DEXIE_DB,
} from "../constants/app";
import * as databaseUtil from "../db/databaseUtil";
import { retrieveSettingsForActiveAccount } from "../db/index";
import { accessToken } from "../libs/crypto";
import { logger } from "../utils/logger";
@@ -173,9 +178,12 @@ export default class PhotoDialog extends Vue {
* @throws {Error} When settings retrieval fails
*/
async mounted() {
logger.log("PhotoDialog mounted");
// logger.log("PhotoDialog mounted");
try {
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.activeDid = settings.activeDid || "";
this.isRegistered = !!settings.isRegistered;
logger.log("isRegistered:", this.isRegistered);

View File

@@ -102,7 +102,12 @@
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { DEFAULT_PUSH_SERVER, NotificationIface } from "../constants/app";
import {
DEFAULT_PUSH_SERVER,
NotificationIface,
USE_DEXIE_DB,
} from "../constants/app";
import * as databaseUtil from "../db/databaseUtil";
import {
logConsoleAndDb,
retrieveSettingsForActiveAccount,
@@ -169,7 +174,10 @@ export default class PushNotificationPermission extends Vue {
this.isVisible = true;
this.pushType = pushType;
try {
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
let pushUrl = DEFAULT_PUSH_SERVER;
if (settings?.webPushServer) {
pushUrl = settings.webPushServer;

View File

@@ -15,7 +15,8 @@
<script lang="ts">
import { Component, Vue, Prop } from "vue-facing-decorator";
import { AppString, NotificationIface } from "../constants/app";
import { AppString, NotificationIface, USE_DEXIE_DB } from "../constants/app";
import * as databaseUtil from "../db/databaseUtil";
import { retrieveSettingsForActiveAccount } from "../db/index";
@Component
@@ -28,7 +29,10 @@ export default class TopMessage extends Vue {
async mounted() {
try {
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
if (
settings.warnIfTestServer &&
settings.apiServer !== AppString.PROD_ENDORSER_API_SERVER

View File

@@ -37,9 +37,11 @@
<script lang="ts">
import { Vue, Component, Prop } from "vue-facing-decorator";
import { NotificationIface } from "../constants/app";
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
import * as databaseUtil from "../db/databaseUtil";
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
@Component
export default class UserNameDialog extends Vue {
@@ -61,15 +63,25 @@ export default class UserNameDialog extends Vue {
*/
async open(aCallback?: (name?: string) => void) {
this.callback = aCallback || this.callback;
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.givenName = settings.firstName || "";
this.visible = true;
}
async onClickSaveChanges() {
await db.settings.update(MASTER_SETTINGS_KEY, {
firstName: this.givenName,
});
const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec(
"UPDATE settings SET firstName = ? WHERE id = ?",
[this.givenName, MASTER_SETTINGS_KEY],
);
if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, {
firstName: this.givenName,
});
}
this.visible = false;
this.callback(this.givenName);
}

View File

@@ -3,6 +3,8 @@ import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader";
import * as SkeletonUtils from "three/addons/utils/SkeletonUtils";
import * as TWEEN from "@tweenjs/tween.js";
import { USE_DEXIE_DB } from "../../../../constants/app";
import * as databaseUtil from "../../../../db/databaseUtil";
import { retrieveSettingsForActiveAccount } from "../../../../db";
import { getHeaders } from "../../../../libs/endorserServer";
import { logger } from "../../../../utils/logger";
@@ -14,7 +16,10 @@ export async function loadLandmarks(vue, world, scene, loop) {
vue.setWorldProperty("animationDurationSeconds", ANIMATION_DURATION_SECS);
try {
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
const activeDid = settings.activeDid || "";
const apiServer = settings.apiServer;
const headers = await getHeaders(activeDid);

View File

@@ -7,6 +7,7 @@ export enum AppString {
// This is used in titles and verbiage inside the app.
// There is also an app name without spaces, for packaging in the package.json file used in the manifest.
APP_NAME = "Time Safari",
APP_NAME_NO_SPACES = "TimeSafari",
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
@@ -43,13 +44,15 @@ export const DEFAULT_PARTNER_API_SERVER =
AppString.TEST_PARTNER_API_SERVER;
export const DEFAULT_PUSH_SERVER =
window.location.protocol + "//" + window.location.host;
import.meta.env.VITE_DEFAULT_PUSH_SERVER || "https://timesafari.app";
export const IMAGE_TYPE_PROFILE = "profile";
export const PASSKEYS_ENABLED =
!!import.meta.env.VITE_PASSKEYS_ENABLED || false;
export const USE_DEXIE_DB = false;
/**
* The possible values for "group" and "type" are in App.vue.
* Some of this comes from the notiwind package, some is custom.

138
src/db-sql/migration.ts Normal file
View File

@@ -0,0 +1,138 @@
import migrationService from "../services/migrationService";
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
import { arrayBufferToBase64 } from "@/libs/crypto";
// Generate a random secret for the secret table
// It's not really secure to maintain the secret next to the user's data.
// However, until we have better hooks into a real wallet or reliable secure
// storage, we'll do this for user convenience. As they sign more records
// and integrate with more people, they'll value it more and want to be more
// secure, so we'll prompt them to take steps to back it up, properly encrypt,
// etc. At the beginning, we'll prompt for a password, then we'll prompt for a
// PWA so it's not in a browser... and then we hope to be integrated with a
// real wallet or something else more secure.
// One might ask: why encrypt at all? We figure a basic encryption is better
// than none. Plus, we expect to support their own password or keystore or
// external wallet as better signing options in the future, so it's gonna be
// important to have the structure where each account access might require
// user action.
// (Once upon a time we stored the secret in localStorage, but it frequently
// got erased, even though the IndexedDB still had the identity data. This
// ended up throwing lots of errors to the user... and they'd end up in a state
// where they couldn't take action because they couldn't unlock that identity.)
const randomBytes = crypto.getRandomValues(new Uint8Array(32));
const secretBase64 = arrayBufferToBase64(randomBytes);
// Each migration can include multiple SQL statements (with semicolons)
const MIGRATIONS = [
{
name: "001_initial",
// see ../db/tables files for explanations of the fields
sql: `
CREATE TABLE IF NOT EXISTS accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
dateCreated TEXT NOT NULL,
derivationPath TEXT,
did TEXT NOT NULL,
identityEncrBase64 TEXT, -- encrypted & base64-encoded
mnemonicEncrBase64 TEXT, -- encrypted & base64-encoded
passkeyCredIdHex TEXT,
publicKeyHex TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_accounts_did ON accounts(did);
CREATE TABLE IF NOT EXISTS secret (
id INTEGER PRIMARY KEY AUTOINCREMENT,
secretBase64 TEXT NOT NULL
);
INSERT INTO secret (id, secretBase64) VALUES (1, '${secretBase64}');
CREATE TABLE IF NOT EXISTS settings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
accountDid TEXT,
activeDid TEXT,
apiServer TEXT,
filterFeedByNearby BOOLEAN,
filterFeedByVisible BOOLEAN,
finishedOnboarding BOOLEAN,
firstName TEXT,
hideRegisterPromptOnNewContact BOOLEAN,
isRegistered BOOLEAN,
lastName TEXT,
lastAckedOfferToUserJwtId TEXT,
lastAckedOfferToUserProjectsJwtId TEXT,
lastNotifiedClaimId TEXT,
lastViewedClaimId TEXT,
notifyingNewActivityTime TEXT,
notifyingReminderMessage TEXT,
notifyingReminderTime TEXT,
partnerApiServer TEXT,
passkeyExpirationMinutes INTEGER,
profileImageUrl TEXT,
searchBoxes TEXT, -- Stored as JSON string
showContactGivesInline BOOLEAN,
showGeneralAdvanced BOOLEAN,
showShortcutBvc BOOLEAN,
vapid TEXT,
warnIfProdServer BOOLEAN,
warnIfTestServer BOOLEAN,
webPushServer TEXT
);
CREATE INDEX IF NOT EXISTS idx_settings_accountDid ON settings(accountDid);
INSERT INTO settings (id, apiServer) VALUES (1, '${DEFAULT_ENDORSER_API_SERVER}');
CREATE TABLE IF NOT EXISTS contacts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
did TEXT NOT NULL,
name TEXT,
contactMethods TEXT, -- Stored as JSON string
nextPubKeyHashB64 TEXT,
notes TEXT,
profileImageUrl TEXT,
publicKeyBase64 TEXT,
seesMe BOOLEAN,
registered BOOLEAN
);
CREATE INDEX IF NOT EXISTS idx_contacts_did ON contacts(did);
CREATE INDEX IF NOT EXISTS idx_contacts_name ON contacts(name);
CREATE TABLE IF NOT EXISTS logs (
date TEXT NOT NULL,
message TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS temp (
id TEXT PRIMARY KEY,
blobB64 TEXT
);
`,
},
];
/**
* @param sqlExec - A function that executes a SQL statement and returns the result
* @param extractMigrationNames - A function that extracts the names (string array) from "select name from migrations"
*/
export async function runMigrations<T>(
sqlExec: (sql: string) => Promise<unknown>,
sqlQuery: (sql: string) => Promise<T>,
extractMigrationNames: (result: T) => Set<string>,
): Promise<void> {
for (const migration of MIGRATIONS) {
migrationService.registerMigration(migration);
}
await migrationService.runMigrations(
sqlExec,
sqlQuery,
extractMigrationNames,
);
}

330
src/db/databaseUtil.ts Normal file
View File

@@ -0,0 +1,330 @@
/**
* This file is the SQL replacement of the index.ts file in the db directory.
* That file will eventually be deleted.
*/
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { MASTER_SETTINGS_KEY, Settings } from "./tables/settings";
import { logger } from "@/utils/logger";
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
import { QueryExecResult } from "@/interfaces/database";
export async function updateDefaultSettings(
settingsChanges: Settings,
): Promise<boolean> {
delete settingsChanges.accountDid; // just in case
// ensure there is no "id" that would override the key
delete settingsChanges.id;
try {
const platformService = PlatformServiceFactory.getInstance();
const { sql, params } = generateUpdateStatement(
settingsChanges,
"settings",
"id = ?",
[MASTER_SETTINGS_KEY],
);
const result = await platformService.dbExec(sql, params);
return result.changes === 1;
} catch (error) {
logger.error("Error updating default settings:", error);
if (error instanceof Error) {
throw error; // Re-throw if it's already an Error with a message
} else {
throw new Error(
`Failed to update settings. We recommend you try again or restart the app.`,
);
}
}
}
export async function updateAccountSettings(
accountDid: string,
settingsChanges: Settings,
): Promise<boolean> {
settingsChanges.accountDid = accountDid;
delete settingsChanges.id; // key off account, not ID
const platform = PlatformServiceFactory.getInstance();
// First try to update existing record
const { sql: updateSql, params: updateParams } = generateUpdateStatement(
settingsChanges,
"settings",
"accountDid = ?",
[accountDid],
);
const updateResult = await platform.dbExec(updateSql, updateParams);
// If no record was updated, insert a new one
if (updateResult.changes === 1) {
return true;
} else {
const columns = Object.keys(settingsChanges);
const values = Object.values(settingsChanges);
const placeholders = values.map(() => "?").join(", ");
const insertSql = `INSERT INTO settings (${columns.join(", ")}) VALUES (${placeholders})`;
const result = await platform.dbExec(insertSql, values);
return result.changes === 1;
}
}
const DEFAULT_SETTINGS: Settings = {
id: MASTER_SETTINGS_KEY,
activeDid: undefined,
apiServer: DEFAULT_ENDORSER_API_SERVER,
};
// retrieves default settings
export async function retrieveSettingsForDefaultAccount(): Promise<Settings> {
const platform = PlatformServiceFactory.getInstance();
const sql = "SELECT * FROM settings WHERE id = ?";
const result = await platform.dbQuery(sql, [MASTER_SETTINGS_KEY]);
if (!result) {
return DEFAULT_SETTINGS;
} else {
const settings = mapColumnsToValues(
result.columns,
result.values,
)[0] as Settings;
if (settings.searchBoxes) {
// @ts-expect-error - the searchBoxes field is a string in the DB
settings.searchBoxes = JSON.parse(settings.searchBoxes);
}
return settings;
}
}
/**
* Retrieves settings for the active account, merging with default settings
*
* @returns Promise<Settings> Combined settings with account-specific overrides
* @throws Will log specific errors for debugging but returns default settings on failure
*/
export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
try {
// Get default settings first
const defaultSettings = await retrieveSettingsForDefaultAccount();
// If no active DID, return defaults
if (!defaultSettings.activeDid) {
logConsoleAndDb(
"[databaseUtil] No active DID found, returning default settings",
);
return defaultSettings;
}
// Get account-specific settings
try {
const platform = PlatformServiceFactory.getInstance();
const result = await platform.dbQuery(
"SELECT * FROM settings WHERE accountDid = ?",
[defaultSettings.activeDid],
);
if (!result?.values?.length) {
logConsoleAndDb(
`[databaseUtil] No account-specific settings found for ${defaultSettings.activeDid}`,
);
return defaultSettings;
}
// Map and filter settings
const overrideSettings = mapColumnsToValues(
result.columns,
result.values,
)[0] as Settings;
const overrideSettingsFiltered = Object.fromEntries(
Object.entries(overrideSettings).filter(([_, v]) => v !== null),
);
// Merge settings
const settings = { ...defaultSettings, ...overrideSettingsFiltered };
// Handle searchBoxes parsing
if (settings.searchBoxes) {
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 = [];
}
}
return settings;
} catch (error) {
logConsoleAndDb(
`[databaseUtil] Failed to retrieve account settings for ${defaultSettings.activeDid}: ${error}`,
true,
);
// Return defaults on error
return defaultSettings;
}
} catch (error) {
logConsoleAndDb(
`[databaseUtil] Failed to retrieve default settings: ${error}`,
true,
);
// Return minimal default settings on complete failure
return {
id: MASTER_SETTINGS_KEY,
activeDid: undefined,
apiServer: DEFAULT_ENDORSER_API_SERVER,
};
}
}
let lastCleanupDate: string | null = null;
export let memoryLogs: string[] = [];
/**
* Logs a message to the database with proper handling of concurrent writes
* @param message - The message to log
* @author Matthew Raymer
*/
export async function logToDb(message: string): Promise<void> {
const platform = PlatformServiceFactory.getInstance();
const todayKey = new Date().toDateString();
const nowKey = new Date().toISOString();
try {
memoryLogs.push(`${new Date().toISOString()} ${message}`);
// Try to insert first, if it fails due to UNIQUE constraint, update instead
await platform.dbExec("INSERT INTO logs (date, message) VALUES (?, ?)", [
nowKey,
message,
]);
// Clean up old logs (keep only last 7 days) - do this less frequently
// Only clean up if the date is different from the last cleanup
if (!lastCleanupDate || lastCleanupDate !== todayKey) {
const sevenDaysAgo = new Date(
new Date().getTime() - 7 * 24 * 60 * 60 * 1000,
);
memoryLogs = memoryLogs.filter(
(log) => log.split(" ")[0] > sevenDaysAgo.toDateString(),
);
await platform.dbExec("DELETE FROM logs WHERE date < ?", [
sevenDaysAgo.toDateString(),
]);
lastCleanupDate = todayKey;
}
} catch (error) {
// Log to console as fallback
// eslint-disable-next-line no-console
console.error(
"Error logging to database:",
error,
" ... for original message:",
message,
);
}
}
// similar method is in the sw_scripts/additional-scripts.js file
export async function logConsoleAndDb(
message: string,
isError = false,
): Promise<void> {
if (isError) {
logger.error(`${new Date().toISOString()} ${message}`);
} else {
logger.log(`${new Date().toISOString()} ${message}`);
}
await logToDb(message);
}
/**
* Generates an SQL INSERT statement and parameters from a model object.
* @param model The model object containing fields to update
* @param tableName The name of the table to update
* @returns Object containing the SQL statement and parameters array
*/
export function generateInsertStatement(
model: Record<string, unknown>,
tableName: string,
): { sql: string; params: unknown[] } {
const columns = Object.keys(model).filter((key) => model[key] !== undefined);
const values = Object.values(model).filter((value) => value !== undefined);
const placeholders = values.map(() => "?").join(", ");
const insertSql = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${placeholders})`;
return {
sql: insertSql,
params: values,
};
}
/**
* Generates an SQL UPDATE statement and parameters from a model object.
* @param model The model object containing fields to update
* @param tableName The name of the table to update
* @param whereClause The WHERE clause for the update (e.g. "id = ?")
* @param whereParams Parameters for the WHERE clause
* @returns Object containing the SQL statement and parameters array
*/
export function generateUpdateStatement(
model: Record<string, unknown>,
tableName: string,
whereClause: string,
whereParams: unknown[] = [],
): { sql: string; params: unknown[] } {
// Filter out undefined/null values and create SET clause
const setClauses: string[] = [];
const params: unknown[] = [];
Object.entries(model).forEach(([key, value]) => {
if (value !== undefined) {
setClauses.push(`${key} = ?`);
params.push(value);
}
});
if (setClauses.length === 0) {
throw new Error("No valid fields to update");
}
const sql = `UPDATE ${tableName} SET ${setClauses.join(", ")} WHERE ${whereClause}`;
return {
sql,
params: [...params, ...whereParams],
};
}
export function mapQueryResultToValues(
record: QueryExecResult | undefined,
): Array<Record<string, unknown>> {
if (!record) {
return [];
}
return mapColumnsToValues(record.columns, record.values) as Array<
Record<string, unknown>
>;
}
/**
* Maps an array of column names to an array of value arrays, creating objects where each column name
* is mapped to its corresponding value.
* @param columns Array of column names to use as object keys
* @param values Array of value arrays, where each inner array corresponds to one row of data
* @returns Array of objects where each object maps column names to their corresponding values
*/
export function mapColumnsToValues(
columns: string[],
values: unknown[][],
): Array<Record<string, unknown>> {
return values.map((row) => {
const obj: Record<string, unknown> = {};
columns.forEach((column, index) => {
obj[column] = row[index];
});
return obj;
});
}

View File

@@ -1,3 +1,9 @@
/**
* This is the original IndexedDB version of the database.
* It will eventually be replaced fully by the SQL version in databaseUtil.ts.
* Turn this on or off with the USE_DEXIE_DB constant in constants/app.ts.
*/
import BaseDexie, { Table } from "dexie";
import { encrypted, Encryption } from "@pvermeer/dexie-encrypted-addon";
import * as R from "ramda";
@@ -26,8 +32,8 @@ type NonsensitiveTables = {
};
// Using 'unknown' instead of 'any' for stricter typing and to avoid TypeScript warnings
export type SecretDexie<T extends unknown = SecretTable> = BaseDexie & T;
export type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T;
type SecretDexie<T extends unknown = SecretTable> = BaseDexie & T;
type SensitiveDexie<T extends unknown = SensitiveTables> = BaseDexie & T;
export type NonsensitiveDexie<T extends unknown = NonsensitiveTables> =
BaseDexie & T;
@@ -90,7 +96,7 @@ db.on("populate", async () => {
try {
await db.settings.add(DEFAULT_SETTINGS);
} catch (error) {
console.error(
logger.error(
"Error populating the database with default settings:",
error,
);
@@ -99,12 +105,12 @@ db.on("populate", async () => {
// Helper function to safely open the database with retries
async function safeOpenDatabase(retries = 1, delay = 500): Promise<void> {
// console.log("Starting safeOpenDatabase with retries:", retries);
// logger.log("Starting safeOpenDatabase with retries:", retries);
for (let i = 0; i < retries; i++) {
try {
// console.log(`Attempt ${i + 1}: Checking if database is open...`);
// logger.log(`Attempt ${i + 1}: Checking if database is open...`);
if (!db.isOpen()) {
// console.log(`Attempt ${i + 1}: Database is closed, attempting to open...`);
// logger.log(`Attempt ${i + 1}: Database is closed, attempting to open...`);
// Create a promise that rejects after 5 seconds
const timeoutPromise = new Promise((_, reject) => {
@@ -113,19 +119,19 @@ async function safeOpenDatabase(retries = 1, delay = 500): Promise<void> {
// Race between the open operation and the timeout
const openPromise = db.open();
// console.log(`Attempt ${i + 1}: Waiting for db.open() promise...`);
// logger.log(`Attempt ${i + 1}: Waiting for db.open() promise...`);
await Promise.race([openPromise, timeoutPromise]);
// If we get here, the open succeeded
// console.log(`Attempt ${i + 1}: Database opened successfully`);
// logger.log(`Attempt ${i + 1}: Database opened successfully`);
return;
}
// console.log(`Attempt ${i + 1}: Database was already open`);
// logger.log(`Attempt ${i + 1}: Database was already open`);
return;
} catch (error) {
console.error(`Attempt ${i + 1}: Database open failed:`, error);
logger.error(`Attempt ${i + 1}: Database open failed:`, error);
if (i < retries - 1) {
console.log(`Attempt ${i + 1}: Waiting ${delay}ms before retry...`);
logger.log(`Attempt ${i + 1}: Waiting ${delay}ms before retry...`);
await new Promise((resolve) => setTimeout(resolve, delay));
} else {
throw error;
@@ -142,16 +148,14 @@ export async function updateDefaultSettings(
delete settingsChanges.id;
try {
try {
// console.log("Database state before open:", db.isOpen() ? "open" : "closed");
// console.log("Database name:", db.name);
// console.log("Database version:", db.verno);
// logger.log("Database state before open:", db.isOpen() ? "open" : "closed");
// logger.log("Database name:", db.name);
// logger.log("Database version:", db.verno);
await safeOpenDatabase();
} catch (openError: unknown) {
console.error("Failed to open database:", openError);
const errorMessage =
openError instanceof Error ? openError.message : String(openError);
logger.error("Failed to open database:", openError, String(openError));
throw new Error(
`Database connection failed: ${errorMessage}. Please try again or restart the app.`,
`The database connection failed. We recommend you try again or restart the app.`,
);
}
const result = await db.settings.update(
@@ -160,11 +164,13 @@ export async function updateDefaultSettings(
);
return result;
} catch (error) {
console.error("Error updating default settings:", error);
logger.error("Error updating default settings:", error);
if (error instanceof Error) {
throw error; // Re-throw if it's already an Error with a message
} else {
throw new Error(`Failed to update settings: ${error}`);
throw new Error(
`Failed to update settings. We recommend you try again or restart the app.`,
);
}
}
}

View File

@@ -45,6 +45,12 @@ export type Account = {
publicKeyHex: string;
};
// When finished with USE_DEXIE_DB, move these fields to Account and move identity and mnemonic here.
export type AccountEncrypted = Account & {
identityEncrBase64: string;
mnemonicEncrBase64: string;
};
/**
* Schema for the accounts table in the database.
* Fields starting with a $ character are encrypted.

View File

@@ -25,6 +25,25 @@ function createWindow(): void {
logger.log("Checking preload path:", preloadPath);
logger.log("Preload exists:", fs.existsSync(preloadPath));
// Log environment and paths
logger.log("process.cwd():", process.cwd());
logger.log("__dirname:", __dirname);
logger.log("app.getAppPath():", app.getAppPath());
logger.log("app.isPackaged:", app.isPackaged);
// List files in __dirname and __dirname/www
try {
logger.log("Files in __dirname:", fs.readdirSync(__dirname));
const wwwDir = path.join(__dirname, "www");
if (fs.existsSync(wwwDir)) {
logger.log("Files in www:", fs.readdirSync(wwwDir));
} else {
logger.log("www directory does not exist in __dirname");
}
} catch (e) {
logger.error("Error reading directories:", e);
}
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 1200,
@@ -88,7 +107,16 @@ function createWindow(): void {
logger.log("process.cwd():", process.cwd());
}
const indexPath = path.join(__dirname, "www", "index.html");
let indexPath = path.resolve(__dirname, "dist-electron", "www", "index.html");
if (!fs.existsSync(indexPath)) {
// Fallback for dev mode
indexPath = path.resolve(
process.cwd(),
"dist-electron",
"www",
"index.html",
);
}
if (isDev) {
logger.log("Loading index from:", indexPath);

View File

@@ -2,24 +2,33 @@ const { contextBridge, ipcRenderer } = require("electron");
const logger = {
log: (message, ...args) => {
// Always log in development, log with context in production
if (process.env.NODE_ENV !== "production") {
/* eslint-disable no-console */
console.log(message, ...args);
console.log(`[Preload] ${message}`, ...args);
/* eslint-enable no-console */
}
},
warn: (message, ...args) => {
if (process.env.NODE_ENV !== "production") {
/* eslint-disable no-console */
console.warn(message, ...args);
/* eslint-enable no-console */
}
// Always log warnings
/* eslint-disable no-console */
console.warn(`[Preload] ${message}`, ...args);
/* eslint-enable no-console */
},
error: (message, ...args) => {
// Always log errors
/* eslint-disable no-console */
console.error(message, ...args); // Errors should always be logged
console.error(`[Preload] ${message}`, ...args);
/* eslint-enable no-console */
},
info: (message, ...args) => {
// Always log info in development, log with context in production
if (process.env.NODE_ENV !== "production") {
/* eslint-disable no-console */
console.info(`[Preload] ${message}`, ...args);
/* eslint-enable no-console */
}
},
};
// Use a more direct path resolution approach
@@ -41,7 +50,10 @@ const getPath = (pathType) => {
}
};
logger.log("Preload script starting...");
logger.info("Preload script starting...");
// Force electron platform in the renderer process
window.process = { env: { VITE_PLATFORM: "electron" } };
try {
contextBridge.exposeInMainWorld("electronAPI", {
@@ -65,6 +77,7 @@ try {
env: {
isElectron: true,
isDev: process.env.NODE_ENV === "development",
platform: "electron", // Explicitly set platform
},
// Path utilities
getBasePath: () => {
@@ -72,7 +85,7 @@ try {
},
});
logger.log("Preload script completed successfully");
logger.info("Preload script completed successfully");
} catch (error) {
logger.error("Error in preload script:", error);
}

59
src/interfaces/absurd-sql.d.ts vendored Normal file
View File

@@ -0,0 +1,59 @@
import type { QueryExecResult, SqlValue } from "./database";
declare module "@jlongster/sql.js" {
interface SQL {
Database: new (path: string, options?: { filename: boolean }) => AbsurdSqlDatabase;
FS: {
mkdir: (path: string) => void;
mount: (fs: any, options: any, path: string) => void;
open: (path: string, flags: string) => any;
close: (stream: any) => void;
};
register_for_idb: (fs: any) => void;
}
interface AbsurdSqlDatabase {
exec: (sql: string, params?: unknown[]) => Promise<QueryExecResult[]>;
run: (
sql: string,
params?: unknown[],
) => Promise<{ changes: number; lastId?: number }>;
}
const initSqlJs: (options?: {
locateFile?: (file: string) => string;
}) => Promise<SQL>;
export default initSqlJs;
}
declare module "absurd-sql" {
import type { SQL } from "@jlongster/sql.js";
export class SQLiteFS {
constructor(fs: any, backend: any);
}
}
declare module "absurd-sql/dist/indexeddb-backend" {
export default class IndexedDBBackend {
constructor();
}
}
declare module "absurd-sql/dist/indexeddb-main-thread" {
export interface SQLiteOptions {
filename?: string;
autoLoad?: boolean;
debug?: boolean;
}
export interface SQLiteDatabase {
exec: (sql: string, params?: unknown[]) => Promise<QueryExecResult[]>;
close: () => Promise<void>;
}
export function initSqlJs(options?: any): Promise<any>;
export function createDatabase(options?: SQLiteOptions): Promise<SQLiteDatabase>;
export function openDatabase(options?: SQLiteOptions): Promise<SQLiteDatabase>;
}

View File

@@ -9,7 +9,6 @@ export interface AgreeVerifiableCredential {
// Note that previous VCs may have additional fields.
// https://endorser.ch/doc/html/transactions.html#id4
export interface GiveVerifiableCredential extends GenericVerifiableCredential {
"@context"?: string;
"@type": "GiveAction";
agent?: { identifier: string };
description?: string;
@@ -19,14 +18,45 @@ export interface GiveVerifiableCredential extends GenericVerifiableCredential {
object?: { amountOfThisGood: number; unitCode: string };
provider?: GenericVerifiableCredential;
recipient?: { identifier: string };
type: string[];
issuer: string;
issuanceDate: string;
credentialSubject: {
id: string;
type: "GiveAction";
offeredBy?: {
type: "Person";
identifier: string;
};
offeredTo?: {
type: "Person";
identifier: string;
};
offeredToProject?: {
type: "Project";
identifier: string;
};
offeredToProjectVisibleToDids?: string[];
offeredToVisibleToDids?: string[];
offeredByVisibleToDids?: string[];
amount: {
type: "QuantitativeValue";
value: number;
unitCode: string;
};
startTime?: string;
endTime?: string;
};
}
// Note that previous VCs may have additional fields.
// https://endorser.ch/doc/html/transactions.html#id8
export interface OfferVerifiableCredential extends GenericVerifiableCredential {
"@context"?: string;
"@type": "Offer";
description?: string;
fulfills?: { "@type": string; identifier?: string; lastClaimId?: string }[];
identifier?: string;
image?: string;
includesObject?: { amountOfThisGood: number; unitCode: string };
itemOffered?: {
description?: string;
@@ -37,9 +67,38 @@ export interface OfferVerifiableCredential extends GenericVerifiableCredential {
name?: string;
};
};
offeredBy?: { identifier: string };
provider?: GenericVerifiableCredential;
recipient?: { identifier: string };
validThrough?: string;
type: string[];
issuer: string;
issuanceDate: string;
credentialSubject: {
id: string;
type: "Offer";
offeredBy?: {
type: "Person";
identifier: string;
};
offeredTo?: {
type: "Person";
identifier: string;
};
offeredToProject?: {
type: "Project";
identifier: string;
};
offeredToProjectVisibleToDids?: string[];
offeredToVisibleToDids?: string[];
offeredByVisibleToDids?: string[];
amount: {
type: "QuantitativeValue";
value: number;
unitCode: string;
};
startTime?: string;
endTime?: string;
};
}
// Note that previous VCs may have additional fields.

View File

@@ -1,6 +1,6 @@
// similar to VerifiableCredentialSubject... maybe rename this
export interface GenericVerifiableCredential {
"@context"?: string;
"@context": string | string[];
"@type": string;
[key: string]: unknown;
}
@@ -34,3 +34,147 @@ export interface ErrorResult extends ResultWithType {
type: "error";
error: InternalError;
}
export interface KeyMeta {
did: string;
name?: string;
publicKeyHex: string;
mnemonic: string;
derivationPath: string;
registered?: boolean;
profileImageUrl?: string;
identity?: string; // Stringified IIdentifier object from Veramo
passkeyCredIdHex?: string; // The Webauthn credential ID in hex, if this is from a passkey
[key: string]: unknown;
}
export interface QuantitativeValue extends GenericVerifiableCredential {
"@type": "QuantitativeValue";
"@context": string | string[];
amountOfThisGood: number;
unitCode: string;
[key: string]: unknown;
}
export interface AxiosErrorResponse {
message?: string;
response?: {
data?: {
error?: {
message?: string;
};
[key: string]: unknown;
};
status?: number;
config?: unknown;
};
config?: unknown;
[key: string]: unknown;
}
export interface UserInfo {
did: string;
name: string;
publicEncKey: string;
registered: boolean;
profileImageUrl?: string;
nextPublicEncKeyHash?: string;
}
export interface CreateAndSubmitClaimResult {
success: boolean;
error?: string;
handleId?: string;
}
export interface PlanSummaryRecord {
handleId: string;
issuer: string;
claim: GenericVerifiableCredential;
[key: string]: unknown;
}
export interface Agent {
identifier?: string;
did?: string;
[key: string]: unknown;
}
export interface ClaimObject {
"@type": string;
"@context"?: string | string[];
fulfills?: Array<{
"@type": string;
identifier?: string;
[key: string]: unknown;
}>;
object?: GenericVerifiableCredential;
agent?: Agent;
participant?: {
identifier?: string;
[key: string]: unknown;
};
identifier?: string;
[key: string]: unknown;
}
export interface VerifiableCredentialClaim {
"@context": string | string[];
"@type": string;
type: string[];
credentialSubject: ClaimObject;
[key: string]: unknown;
}
export interface GiveVerifiableCredential extends GenericVerifiableCredential {
"@type": "GiveAction";
"@context": string | string[];
object?: GenericVerifiableCredential;
agent?: Agent;
participant?: {
identifier?: string;
[key: string]: unknown;
};
fulfills?: Array<{
"@type": string;
identifier?: string;
[key: string]: unknown;
}>;
[key: string]: unknown;
}
export interface OfferVerifiableCredential extends GenericVerifiableCredential {
"@type": "OfferAction";
"@context": string | string[];
object?: GenericVerifiableCredential;
agent?: Agent;
participant?: {
identifier?: string;
[key: string]: unknown;
};
itemOffered?: {
description?: string;
isPartOf?: {
"@type": string;
identifier: string;
[key: string]: unknown;
};
[key: string]: unknown;
};
[key: string]: unknown;
}
export interface RegisterVerifiableCredential
extends GenericVerifiableCredential {
"@type": "RegisterAction";
"@context": string | string[];
agent: {
identifier: string;
};
object: string;
participant?: {
identifier: string;
};
identifier?: string;
[key: string]: unknown;
}

View File

@@ -0,0 +1,15 @@
export type SqlValue = string | number | null | Uint8Array;
export interface QueryExecResult {
columns: Array<string>;
values: Array<Array<SqlValue>>;
}
export interface DatabaseService {
initialize(): Promise<void>;
query(sql: string, params?: unknown[]): Promise<QueryExecResult[]>;
run(
sql: string,
params?: unknown[],
): Promise<{ changes: number; lastId?: number }>;
}

View File

@@ -1,7 +1,37 @@
export * from "./claims";
export * from "./claims-result";
export * from "./common";
export type {
// From common.ts
GenericCredWrapper,
GenericVerifiableCredential,
KeyMeta,
// Exclude types that are also exported from other files
// GiveVerifiableCredential,
// OfferVerifiableCredential,
// RegisterVerifiableCredential,
// PlanSummaryRecord,
// UserInfo,
} from "./common";
export type {
// From claims.ts
GiveVerifiableCredential,
OfferVerifiableCredential,
RegisterVerifiableCredential,
} from "./claims";
export type {
// From claims-result.ts
CreateAndSubmitClaimResult,
} from "./claims-result";
export type {
// From records.ts
PlanSummaryRecord,
} from "./records";
export type {
// From user.ts
UserInfo,
} from "./user";
export * from "./limits";
export * from "./records";
export * from "./user";
export * from "./deepLinks";

View File

@@ -159,7 +159,7 @@ export const nextDerivationPath = (origDerivPath: string) => {
};
// Base64 encoding/decoding utilities for browser
function base64ToArrayBuffer(base64: string): Uint8Array {
export function base64ToArrayBuffer(base64: string): Uint8Array {
const binaryString = atob(base64);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
@@ -168,7 +168,7 @@ function base64ToArrayBuffer(base64: string): Uint8Array {
return bytes;
}
function arrayBufferToBase64(buffer: ArrayBuffer): string {
export function arrayBufferToBase64(buffer: ArrayBuffer): string {
const binary = String.fromCharCode(...new Uint8Array(buffer));
return btoa(binary);
}
@@ -178,7 +178,7 @@ const IV_LENGTH = 12;
const KEY_LENGTH = 256;
const ITERATIONS = 100000;
// Encryption helper function
// Message encryption helper function, used for onboarding meeting messages
export async function encryptMessage(message: string, password: string) {
const encoder = new TextEncoder();
const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
@@ -226,7 +226,7 @@ export async function encryptMessage(message: string, password: string) {
return btoa(JSON.stringify(result));
}
// Decryption helper function
// Message decryption helper function, used for onboarding meeting messages
export async function decryptMessage(encryptedJson: string, password: string) {
const decoder = new TextDecoder();
const { salt, iv, encrypted } = JSON.parse(atob(encryptedJson));
@@ -273,7 +273,7 @@ export async function decryptMessage(encryptedJson: string, password: string) {
}
// Test function to verify encryption/decryption
export async function testEncryptionDecryption() {
export async function testMessageEncryptionDecryption() {
try {
const testMessage = "Hello, this is a test message! 🚀";
const testPassword = "myTestPassword123";
@@ -299,9 +299,111 @@ export async function testEncryptionDecryption() {
logger.log("\nTesting with wrong password...");
try {
await decryptMessage(encrypted, "wrongPassword");
logger.log("Should not reach here");
logger.log("Incorrectly decrypted with wrong password ❌");
} catch (error) {
logger.log("Correctly failed with wrong password ✅");
logger.log("Correctly failed to decrypt with wrong password ✅");
}
return success;
} catch (error) {
logger.error("Test failed with error:", error);
return false;
}
}
// Simple encryption using Node's crypto, used for the initial encryption of the identity and mnemonic
export async function simpleEncrypt(
text: string,
secret: ArrayBuffer,
): Promise<ArrayBuffer> {
const iv = crypto.getRandomValues(new Uint8Array(16));
// Derive a 256-bit key from the secret using SHA-256
const keyData = await crypto.subtle.digest("SHA-256", secret);
const key = await crypto.subtle.importKey(
"raw",
keyData,
{ name: "AES-GCM" },
false,
["encrypt"],
);
const encrypted = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
new TextEncoder().encode(text),
);
// Combine IV and encrypted data
const result = new Uint8Array(iv.length + encrypted.byteLength);
result.set(iv);
result.set(new Uint8Array(encrypted), iv.length);
return result.buffer;
}
// Simple decryption using Node's crypto, used for the default decryption of identity and mnemonic
export async function simpleDecrypt(
encryptedText: ArrayBuffer,
secret: ArrayBuffer,
): Promise<string> {
const data = new Uint8Array(encryptedText);
// Extract IV and encrypted data
const iv = data.slice(0, 16);
const encrypted = data.slice(16);
// Derive the same 256-bit key from the secret using SHA-256
const keyData = await crypto.subtle.digest("SHA-256", secret);
const key = await crypto.subtle.importKey(
"raw",
keyData,
{ name: "AES-GCM" },
false,
["decrypt"],
);
const decrypted = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv },
key,
encrypted,
);
return new TextDecoder().decode(decrypted);
}
// Test function for simple encryption/decryption
export async function testSimpleEncryptionDecryption() {
try {
const testMessage = "Hello, this is a test message! 🚀";
const testSecret = crypto.getRandomValues(new Uint8Array(32));
logger.log("Original message:", testMessage);
// Test encryption
logger.log("Encrypting...");
const encrypted = await simpleEncrypt(testMessage, testSecret);
const encryptedBase64 = arrayBufferToBase64(encrypted);
logger.log("Encrypted result:", encryptedBase64);
// Test decryption
logger.log("Decrypting...");
const encryptedArrayBuffer = base64ToArrayBuffer(encryptedBase64);
const decrypted = await simpleDecrypt(encryptedArrayBuffer, testSecret);
logger.log("Decrypted result:", decrypted);
// Verify
const success = testMessage === decrypted;
logger.log("Test " + (success ? "PASSED ✅" : "FAILED ❌"));
logger.log("Messages match:", success);
// Test with wrong secret
logger.log("\nTesting with wrong secret...");
try {
await simpleDecrypt(encryptedArrayBuffer, new Uint8Array(32));
logger.log("Incorrectly decrypted with wrong secret ❌");
} catch (error) {
logger.log("Correctly failed to decrypt with wrong secret ✅");
}
return success;

View File

@@ -17,29 +17,12 @@ import { didEthLocalResolver } from "./did-eth-local-resolver";
import { PEER_DID_PREFIX, verifyPeerSignature } from "./didPeer";
import { base64urlDecodeString, createDidPeerJwt } from "./passkeyDidPeer";
import { urlBase64ToUint8Array } from "./util";
import { KeyMeta } from "../../../interfaces/common";
export const ETHR_DID_PREFIX = "did:ethr:";
export const JWT_VERIFY_FAILED_CODE = "JWT_VERIFY_FAILED";
export const UNSUPPORTED_DID_METHOD_CODE = "UNSUPPORTED_DID_METHOD";
/**
* Meta info about a key
*/
export interface KeyMeta {
/**
* Decentralized ID for the key
*/
did: string;
/**
* Stringified IIDentifier object from Veramo
*/
identity?: string;
/**
* The Webauthn credential ID in hex, if this is from a passkey
*/
passkeyCredIdHex?: string;
}
const ethLocalResolver = new Resolver({ ethr: didEthLocalResolver });
/**

View File

@@ -1,7 +1,6 @@
import { Buffer } from "buffer/";
import { JWTPayload } from "did-jwt";
import { DIDResolutionResult } from "did-resolver";
import { sha256 } from "ethereum-cryptography/sha256.js";
import { p256 } from "@noble/curves/p256";
import {
startAuthentication,
startRegistration,
@@ -11,12 +10,13 @@ import {
generateRegistrationOptions,
verifyAuthenticationResponse,
verifyRegistrationResponse,
VerifyAuthenticationResponseOpts,
} from "@simplewebauthn/server";
import { VerifyAuthenticationResponseOpts } from "@simplewebauthn/server/esm/authentication/verifyAuthenticationResponse";
import {
Base64URLString,
PublicKeyCredentialCreationOptionsJSON,
PublicKeyCredentialRequestOptionsJSON,
AuthenticatorAssertionResponse,
} from "@simplewebauthn/types";
import { AppString } from "../../../constants/app";
@@ -194,16 +194,19 @@ export class PeerSetup {
},
};
const credential = await navigator.credentials.get(options);
const credential = (await navigator.credentials.get(
options,
)) as PublicKeyCredential;
// console.log("nav credential get", credential);
this.authenticatorData = credential?.response.authenticatorData;
const response = credential?.response as AuthenticatorAssertionResponse;
this.authenticatorData = response?.authenticatorData;
const authenticatorDataBase64Url = arrayBufferToBase64URLString(
this.authenticatorData as ArrayBuffer,
);
this.clientDataJsonBase64Url = arrayBufferToBase64URLString(
credential?.response.clientDataJSON,
response?.clientDataJSON,
);
// Our custom type of JWANT means the signature is based on a concatenation of the two Webauthn properties
@@ -228,9 +231,7 @@ export class PeerSetup {
.replace(/\//g, "_")
.replace(/=+$/, "");
const origSignature = Buffer.from(credential?.response.signature).toString(
"base64",
);
const origSignature = Buffer.from(response?.signature).toString("base64");
this.signature = origSignature
.replace(/\+/g, "-")
.replace(/\//g, "_")
@@ -315,24 +316,18 @@ export async function createDidPeerJwt(
// ... and this import:
// import { p256 } from "@noble/curves/p256";
export async function verifyJwtP256(
credIdHex: string,
issuerDid: string,
authenticatorData: ArrayBuffer,
challenge: Uint8Array,
clientDataJsonBase64Url: Base64URLString,
signature: Base64URLString,
) {
const authDataFromBase = Buffer.from(authenticatorData);
const clientDataFromBase = Buffer.from(clientDataJsonBase64Url, "base64");
const sigBuffer = Buffer.from(signature, "base64");
const finalSigBuffer = unwrapEC2Signature(sigBuffer);
const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid);
// Hash the client data
const hash = sha256(clientDataFromBase);
// Construct the preimage
const preimage = Buffer.concat([authDataFromBase, hash]);
// Use challenge in preimage construction
const preimage = Buffer.concat([authDataFromBase, Buffer.from(challenge)]);
const isValid = p256.verify(
finalSigBuffer,
@@ -383,122 +378,37 @@ export async function verifyJwtSimplewebauthn(
// similar code is in endorser-ch util-crypto.ts verifyPeerSignature
export async function verifyJwtWebCrypto(
credId: Base64URLString,
issuerDid: string,
authenticatorData: ArrayBuffer,
challenge: Uint8Array,
clientDataJsonBase64Url: Base64URLString,
signature: Base64URLString,
) {
const authDataFromBase = Buffer.from(authenticatorData);
const clientDataFromBase = Buffer.from(clientDataJsonBase64Url, "base64");
const sigBuffer = Buffer.from(signature, "base64");
const finalSigBuffer = unwrapEC2Signature(sigBuffer);
// Hash the client data
const hash = sha256(clientDataFromBase);
// Use challenge in preimage construction
const preimage = Buffer.concat([authDataFromBase, Buffer.from(challenge)]);
// Construct the preimage
const preimage = Buffer.concat([authDataFromBase, hash]);
return verifyPeerSignature(preimage, issuerDid, finalSigBuffer);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function peerDidToDidDocument(did: string): Promise<DIDResolutionResult> {
if (!did.startsWith("did:peer:0z")) {
throw new Error(
"This only verifies a peer DID, method 0, encoded base58btc.",
);
}
// this is basically hard-coded from https://www.w3.org/TR/did-core/#example-various-verification-method-types
// (another reference is the @aviarytech/did-peer resolver)
// Remove unused functions:
// - peerDidToDidDocument
// - COSEtoPEM
// - base64urlDecodeArrayBuffer
// - base64urlEncodeArrayBuffer
// - pemToCryptoKey
/**
* Looks like JsonWebKey2020 isn't too difficult:
* - change context security/suites link to jws-2020/v1
* - change publicKeyMultibase to publicKeyJwk generated with cborToKeys
* - change type to JsonWebKey2020
*/
const id = did.split(":")[2];
const multibase = id.slice(1);
const encnumbasis = multibase.slice(1);
const didDocument = {
"@context": [
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/suites/secp256k1-2019/v1",
],
assertionMethod: [did + "#" + encnumbasis],
authentication: [did + "#" + encnumbasis],
capabilityDelegation: [did + "#" + encnumbasis],
capabilityInvocation: [did + "#" + encnumbasis],
id: did,
keyAgreement: undefined,
service: undefined,
verificationMethod: [
{
controller: did,
id: did + "#" + encnumbasis,
publicKeyMultibase: multibase,
type: "EcdsaSecp256k1VerificationKey2019",
},
],
};
return {
didDocument,
didDocumentMetadata: {},
didResolutionMetadata: { contentType: "application/did+ld+json" },
};
}
// convert COSE public key to PEM format
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function COSEtoPEM(cose: Buffer) {
// const alg = cose.get(3); // Algorithm
const x = cose[-2]; // x-coordinate
const y = cose[-3]; // y-coordinate
// Ensure the coordinates are in the correct format
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error because it complains about the type of x and y
const pubKeyBuffer = Buffer.concat([Buffer.from([0x04]), x, y]);
// Convert to PEM format
const pem = `-----BEGIN PUBLIC KEY-----
${pubKeyBuffer.toString("base64")}
-----END PUBLIC KEY-----`;
return pem;
}
// tried the base64url library but got an error using their Buffer
// Keep only the used functions:
export function base64urlDecodeString(input: string) {
return atob(input.replace(/-/g, "+").replace(/_/g, "/"));
}
// tried the base64url library but got an error using their Buffer
export function base64urlEncodeString(input: string) {
return btoa(input).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function base64urlDecodeArrayBuffer(input: string) {
input = input.replace(/-/g, "+").replace(/_/g, "/");
const pad = input.length % 4 === 0 ? "" : "====".slice(input.length % 4);
const str = atob(input + pad);
const bytes = new Uint8Array(str.length);
for (let i = 0; i < str.length; i++) {
bytes[i] = str.charCodeAt(i);
}
return bytes.buffer;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function base64urlEncodeArrayBuffer(buffer: ArrayBuffer) {
const str = String.fromCharCode(...new Uint8Array(buffer));
return base64urlEncodeString(str);
}
// from @simplewebauthn/browser
function arrayBufferToBase64URLString(buffer: ArrayBuffer) {
const bytes = new Uint8Array(buffer);
@@ -523,28 +433,3 @@ function base64URLStringToArrayBuffer(base64URLString: string) {
}
return buffer;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function pemToCryptoKey(pem: string) {
const binaryDerString = atob(
pem
.split("\n")
.filter((x) => !x.includes("-----"))
.join(""),
);
const binaryDer = new Uint8Array(binaryDerString.length);
for (let i = 0; i < binaryDerString.length; i++) {
binaryDer[i] = binaryDerString.charCodeAt(i);
}
// console.log("binaryDer", binaryDer.buffer);
return await window.crypto.subtle.importKey(
"spki",
binaryDer.buffer,
{
name: "RSASSA-PKCS1-v1_5",
hash: "SHA-256",
},
true,
["verify"],
);
}

View File

@@ -26,29 +26,37 @@ import {
DEFAULT_IMAGE_API_SERVER,
NotificationIface,
APP_SERVER,
USE_DEXIE_DB,
} from "../constants/app";
import { Contact } from "../db/tables/contacts";
import { accessToken, deriveAddress, nextDerivationPath } from "../libs/crypto";
import { logConsoleAndDb, NonsensitiveDexie } from "../db/index";
import { NonsensitiveDexie } from "../db/index";
import { logConsoleAndDb } from "../db/databaseUtil";
import {
retrieveAccountMetadata,
retrieveFullyDecryptedAccount,
getPasskeyExpirationSeconds,
} from "../libs/util";
import { createEndorserJwtForKey, KeyMeta } from "../libs/crypto/vc";
import { createEndorserJwtForKey } from "../libs/crypto/vc";
import { KeyMeta } from "../interfaces/common";
import {
GenericCredWrapper,
GenericVerifiableCredential,
AxiosErrorResponse,
UserInfo,
CreateAndSubmitClaimResult,
PlanSummaryRecord,
GiveVerifiableCredential,
OfferVerifiableCredential,
RegisterVerifiableCredential,
GenericVerifiableCredential,
GenericCredWrapper,
PlanSummaryRecord,
UserInfo,
CreateAndSubmitClaimResult,
} from "../interfaces";
ClaimObject,
VerifiableCredentialClaim,
Agent,
QuantitativeValue,
} from "../interfaces/common";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
/**
* Standard context for schema.org data
@@ -100,7 +108,10 @@ export const ENDORSER_CH_HANDLE_PREFIX = "https://endorser.ch/entity/";
export const BLANK_GENERIC_SERVER_RECORD: GenericCredWrapper<GenericVerifiableCredential> =
{
claim: { "@type": "" },
claim: {
"@context": SCHEMA_ORG_CONTEXT,
"@type": "",
},
handleId: "",
id: "",
issuedAt: "",
@@ -125,7 +136,7 @@ export function isDid(did: string): boolean {
* @param {string} did - The DID to check
* @returns {boolean} True if DID is hidden
*/
export function isHiddenDid(did: string): boolean {
export function isHiddenDid(did: string | undefined): boolean {
return did === HIDDEN_DID;
}
@@ -180,37 +191,21 @@ export function isEmptyOrHiddenDid(did?: string): boolean {
* };
* testRecursivelyOnStrings(isHiddenDid, obj); // Returns: true
*/
function testRecursivelyOnStrings(
func: (arg0: unknown) => boolean,
const testRecursivelyOnStrings = (
input: unknown,
): boolean {
// Test direct string values
if (Object.prototype.toString.call(input) === "[object String]") {
return func(input);
test: (s: string) => boolean,
): boolean => {
if (typeof input === "string") {
return test(input);
} else if (Array.isArray(input)) {
return input.some((item) => testRecursivelyOnStrings(item, test));
} else if (input && typeof input === "object") {
return Object.values(input as Record<string, unknown>).some((value) =>
testRecursivelyOnStrings(value, test),
);
}
// Recursively test objects and arrays
else if (input instanceof Object) {
if (!Array.isArray(input)) {
// Handle plain objects
for (const key in input) {
if (testRecursivelyOnStrings(func, input[key])) {
return true;
}
}
} else {
// Handle arrays
for (const value of input) {
if (testRecursivelyOnStrings(func, value)) {
return true;
}
}
}
return false;
} else {
// Non-string, non-object values can't contain strings
return false;
}
}
return false;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function containsHiddenDid(obj: any) {
@@ -551,7 +546,11 @@ export async function setPlanInCache(
* @returns {string|undefined} User-friendly message or undefined if none found
*/
export function serverMessageForUser(error: unknown): string | undefined {
return error?.response?.data?.error?.message;
if (error && typeof error === "object" && "response" in error) {
const err = error as AxiosErrorResponse;
return err.response?.data?.error?.message;
}
return undefined;
}
/**
@@ -573,18 +572,27 @@ export function errorStringForLog(error: unknown) {
// --- property '_value' closes the circle
}
let fullError = "" + error + " - JSON: " + stringifiedError;
const errorResponseText = JSON.stringify(error.response);
// for some reason, error.response is not included in stringify result (eg. for 400 errors on invite redemptions)
if (!R.empty(errorResponseText) && !fullError.includes(errorResponseText)) {
// add error.response stuff
if (R.equals(error?.config, error?.response?.config)) {
// but exclude "config" because it's already in there
const newErrorResponseText = JSON.stringify(
R.omit(["config"] as never[], error.response),
);
fullError += " - .response w/o same config JSON: " + newErrorResponseText;
} else {
fullError += " - .response JSON: " + errorResponseText;
if (error && typeof error === "object" && "response" in error) {
const err = error as AxiosErrorResponse;
const errorResponseText = JSON.stringify(err.response);
// for some reason, error.response is not included in stringify result (eg. for 400 errors on invite redemptions)
if (!R.empty(errorResponseText) && !fullError.includes(errorResponseText)) {
// add error.response stuff
if (
err.response?.config &&
err.config &&
R.equals(err.config, err.response.config)
) {
// but exclude "config" because it's already in there
const newErrorResponseText = JSON.stringify(
R.omit(["config"] as never[], err.response),
);
fullError +=
" - .response w/o same config JSON: " + newErrorResponseText;
} else {
fullError += " - .response JSON: " + errorResponseText;
}
}
}
return fullError;
@@ -650,70 +658,89 @@ export function hydrateGive(
unitCode?: string,
fulfillsProjectHandleId?: string,
fulfillsOfferHandleId?: string,
isTrade: boolean = false, // remove, because this app is all for gifting
isTrade: boolean = false,
imageUrl?: string,
providerPlanHandleId?: string,
lastClaimId?: string,
): GiveVerifiableCredential {
// Remember: replace values or erase if it's null
const vcClaim: GiveVerifiableCredential = vcClaimOrig
? R.clone(vcClaimOrig)
: {
"@context": SCHEMA_ORG_CONTEXT,
"@type": "GiveAction",
object: undefined,
agent: undefined,
fulfills: [],
};
if (lastClaimId) {
// this is an edit
vcClaim.lastClaimId = lastClaimId;
delete vcClaim.identifier;
}
vcClaim.agent = fromDid ? { identifier: fromDid } : undefined;
vcClaim.recipient = toDid ? { identifier: toDid } : undefined;
vcClaim.description = description || undefined;
vcClaim.object =
amount && !isNaN(amount)
? { amountOfThisGood: amount, unitCode: unitCode || "HUR" }
: undefined;
// ensure fulfills is an array
if (!Array.isArray(vcClaim.fulfills)) {
vcClaim.fulfills = vcClaim.fulfills ? [vcClaim.fulfills] : [];
if (fromDid) {
vcClaim.agent = { identifier: fromDid };
}
// ... and replace or add each element, ending with Trade or Donate
// I realize the following doesn't change any elements that are not PlanAction or Offer or Trade/Action.
if (toDid) {
vcClaim.recipient = { identifier: toDid };
}
vcClaim.description = description || undefined;
if (amount && !isNaN(amount)) {
const quantitativeValue: QuantitativeValue = {
"@context": SCHEMA_ORG_CONTEXT,
"@type": "QuantitativeValue",
amountOfThisGood: amount,
unitCode: unitCode || "HUR",
};
vcClaim.object = quantitativeValue;
}
// Initialize fulfills array if not present
if (!Array.isArray(vcClaim.fulfills)) {
vcClaim.fulfills = [];
}
// Filter and add fulfills elements
vcClaim.fulfills = vcClaim.fulfills.filter(
(elem) => elem["@type"] !== "PlanAction",
(elem: { "@type": string }) => elem["@type"] !== "PlanAction",
);
if (fulfillsProjectHandleId) {
vcClaim.fulfills.push({
"@type": "PlanAction",
identifier: fulfillsProjectHandleId,
});
}
vcClaim.fulfills = vcClaim.fulfills.filter(
(elem) => elem["@type"] !== "Offer",
(elem: { "@type": string }) => elem["@type"] !== "Offer",
);
if (fulfillsOfferHandleId) {
vcClaim.fulfills.push({
"@type": "Offer",
identifier: fulfillsOfferHandleId,
});
}
// do Trade/Donate last because current endorser.ch only looks at the first for plans & offers
vcClaim.fulfills = vcClaim.fulfills.filter(
(elem) =>
(elem: { "@type": string }) =>
elem["@type"] !== "DonateAction" && elem["@type"] !== "TradeAction",
);
vcClaim.fulfills.push({ "@type": isTrade ? "TradeAction" : "DonateAction" });
vcClaim.fulfills.push({
"@type": isTrade ? "TradeAction" : "DonateAction",
});
vcClaim.image = imageUrl || undefined;
vcClaim.provider = providerPlanHandleId
? { "@type": "PlanAction", identifier: providerPlanHandleId }
: undefined;
if (providerPlanHandleId) {
vcClaim.provider = {
"@type": "PlanAction",
identifier: providerPlanHandleId,
};
}
return vcClaim;
}
@@ -826,29 +853,38 @@ export function hydrateOffer(
validThrough?: string,
lastClaimId?: string,
): OfferVerifiableCredential {
// Remember: replace values or erase if it's null
const vcClaim: OfferVerifiableCredential = vcClaimOrig
? R.clone(vcClaimOrig)
: {
"@context": SCHEMA_ORG_CONTEXT,
"@type": "Offer",
"@type": "OfferAction",
object: undefined,
agent: undefined,
itemOffered: {},
};
if (lastClaimId) {
// this is an edit
vcClaim.lastClaimId = lastClaimId;
delete vcClaim.identifier;
}
vcClaim.offeredBy = fromDid ? { identifier: fromDid } : undefined;
vcClaim.recipient = toDid ? { identifier: toDid } : undefined;
if (fromDid) {
vcClaim.agent = { identifier: fromDid };
}
if (toDid) {
vcClaim.recipient = { identifier: toDid };
}
vcClaim.description = conditionDescription || undefined;
vcClaim.includesObject =
amount && !isNaN(amount)
? { amountOfThisGood: amount, unitCode: unitCode || "HUR" }
: undefined;
if (amount && !isNaN(amount)) {
const quantitativeValue: QuantitativeValue = {
"@context": SCHEMA_ORG_CONTEXT,
"@type": "QuantitativeValue",
amountOfThisGood: amount,
unitCode: unitCode || "HUR",
};
vcClaim.object = quantitativeValue;
}
if (itemDescription || fulfillsProjectHandleId) {
vcClaim.itemOffered = vcClaim.itemOffered || {};
@@ -860,6 +896,7 @@ export function hydrateOffer(
};
}
}
vcClaim.validThrough = validThrough || undefined;
return vcClaim;
@@ -988,20 +1025,19 @@ export async function createAndSubmitClaim(
},
});
return { type: "success", response };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
return { success: true, handleId: response.data?.handleId };
} catch (error: unknown) {
logger.error("Error submitting claim:", error);
const errorMessage: string =
serverMessageForUser(error) ||
error.message ||
(error && typeof error === "object" && "message" in error
? String(error.message)
: undefined) ||
"Got some error submitting the claim. Check your permissions, network, and error logs.";
return {
type: "error",
error: {
error: errorMessage,
},
success: false,
error: errorMessage,
};
}
}
@@ -1031,12 +1067,9 @@ export async function generateEndorserJwtUrlForAccount(
}
// Add the next key -- not recommended for the QR code for such a high resolution
if (isContact && account?.mnemonic && account?.derivationPath) {
const newDerivPath = nextDerivationPath(account.derivationPath as string);
const nextPublicHex = deriveAddress(
account.mnemonic as string,
newDerivPath,
)[2];
if (isContact) {
const newDerivPath = nextDerivationPath(account.derivationPath);
const nextPublicHex = deriveAddress(account.mnemonic, newDerivPath)[2];
const nextPublicEncKey = Buffer.from(nextPublicHex, "hex");
const nextPublicEncKeyHash = sha256(nextPublicEncKey);
const nextPublicEncKeyHashBase64 =
@@ -1104,21 +1137,21 @@ export const capitalizeAndInsertSpacesBeforeCaps = (text: string) => {
similar code is also contained in endorser-mobile
**/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const claimSummary = (
claim: GenericCredWrapper<GenericVerifiableCredential>,
claim:
| GenericVerifiableCredential
| GenericCredWrapper<GenericVerifiableCredential>,
) => {
if (!claim) {
// to differentiate from "something" above
return "something";
}
let specificClaim:
| GenericVerifiableCredential
| GenericCredWrapper<GenericVerifiableCredential> = claim;
if (claim.claim) {
// probably a Verified Credential
// eslint-disable-next-line @typescript-eslint/no-explicit-any
specificClaim = claim.claim;
let specificClaim: GenericVerifiableCredential;
if ("claim" in claim) {
// It's a GenericCredWrapper
specificClaim = claim.claim as GenericVerifiableCredential;
} else {
// It's already a GenericVerifiableCredential
specificClaim = claim;
}
if (Array.isArray(specificClaim)) {
if (specificClaim.length === 1) {
@@ -1154,99 +1187,103 @@ export const claimSpecialDescription = (
contacts: Array<Contact>,
) => {
let claim = record.claim;
if (claim.claim) {
// it's probably a Verified Credential
claim = claim.claim;
if ("claim" in claim) {
claim = claim.claim as GenericVerifiableCredential;
}
const issuer = didInfo(record.issuer, activeDid, identifiers, contacts);
const type = claim["@type"] || "UnknownType";
const claimObj = claim as ClaimObject;
const type = claimObj["@type"] || "UnknownType";
if (type === "AgreeAction") {
return issuer + " agreed with " + claimSummary(claim.object);
return (
issuer +
" agreed with " +
claimSummary(claimObj.object as GenericVerifiableCredential)
);
} else if (isAccept(claim)) {
return issuer + " accepted " + claimSummary(claim.object);
return (
issuer +
" accepted " +
claimSummary(claimObj.object as GenericVerifiableCredential)
);
} else if (type === "GiveAction") {
// agent.did is for legacy data, before March 2023
const giver = claim.agent?.identifier || claim.agent?.did;
const giverInfo = didInfo(giver, activeDid, identifiers, contacts);
let gaveAmount = claim.object?.amountOfThisGood
? displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
const giveClaim = claim as GiveVerifiableCredential;
const agent: Agent = giveClaim.agent || {
identifier: undefined,
did: undefined,
};
const agentDid = agent.did || agent.identifier;
const contactInfo = agentDid
? didInfo(agentDid, activeDid, identifiers, contacts)
: "someone";
const offering = giveClaim.object
? " " + claimSummary(giveClaim.object)
: "";
if (claim.description) {
if (gaveAmount) {
gaveAmount = gaveAmount + ", and also: ";
}
gaveAmount = gaveAmount + claim.description;
}
if (!gaveAmount) {
gaveAmount = "something not described";
}
// recipient.did is for legacy data, before March 2023
const gaveRecipientId = claim.recipient?.identifier || claim.recipient?.did;
const gaveRecipientInfo = gaveRecipientId
? " to " + didInfo(gaveRecipientId, activeDid, identifiers, contacts)
const recipient = giveClaim.participant?.identifier;
const recipientInfo = recipient
? " to " + didInfo(recipient, activeDid, identifiers, contacts)
: "";
return giverInfo + " gave" + gaveRecipientInfo + ": " + gaveAmount;
return contactInfo + " gave" + offering + recipientInfo;
} else if (type === "JoinAction") {
// agent.did is for legacy data, before March 2023
const agent = claim.agent?.identifier || claim.agent?.did;
const contactInfo = didInfo(agent, activeDid, identifiers, contacts);
let eventOrganizer =
claim.event && claim.event.organizer && claim.event.organizer.name;
eventOrganizer = eventOrganizer || "";
let eventName = claim.event && claim.event.name;
eventName = eventName ? " " + eventName : "";
let fullEvent = eventOrganizer + eventName;
fullEvent = fullEvent ? " attended the " + fullEvent : "";
let eventDate = claim.event && claim.event.startTime;
eventDate = eventDate ? " at " + eventDate : "";
return contactInfo + fullEvent + eventDate;
const joinClaim = claim as ClaimObject;
const agent: Agent = joinClaim.agent || {
identifier: undefined,
did: undefined,
};
const agentDid = agent.did || agent.identifier;
const contactInfo = agentDid
? didInfo(agentDid, activeDid, identifiers, contacts)
: "someone";
const object = joinClaim.object as GenericVerifiableCredential;
const objectInfo = object ? " " + claimSummary(object) : "";
return contactInfo + " joined" + objectInfo;
} else if (isOffer(claim)) {
const offerer = claim.offeredBy?.identifier;
const contactInfo = didInfo(offerer, activeDid, identifiers, contacts);
let offering = "";
if (claim.includesObject) {
offering +=
" " +
displayAmount(
claim.includesObject.unitCode,
claim.includesObject.amountOfThisGood,
);
}
if (claim.itemOffered?.description) {
offering += ", saying: " + claim.itemOffered?.description;
}
// recipient.did is for legacy data, before March 2023
const offerRecipientId =
claim.recipient?.identifier || claim.recipient?.did;
const offerClaim = claim as OfferVerifiableCredential;
const agent: Agent = offerClaim.agent || {
identifier: undefined,
did: undefined,
};
const agentDid = agent.did || agent.identifier;
const contactInfo = agentDid
? didInfo(agentDid, activeDid, identifiers, contacts)
: "someone";
const offering = offerClaim.object
? " " + claimSummary(offerClaim.object)
: "";
const offerRecipientId = offerClaim.participant?.identifier;
const offerRecipientInfo = offerRecipientId
? " to " + didInfo(offerRecipientId, activeDid, identifiers, contacts)
: "";
return contactInfo + " offered" + offering + offerRecipientInfo;
} else if (type === "PlanAction") {
const claimer = claim.agent?.identifier || record.issuer;
const claimerInfo = didInfo(claimer, activeDid, identifiers, contacts);
return claimerInfo + " announced a project: " + claim.name;
const planClaim = claim as ClaimObject;
const agent: Agent = planClaim.agent || {
identifier: undefined,
did: undefined,
};
const agentDid = agent.did || agent.identifier;
const contactInfo = agentDid
? didInfo(agentDid, activeDid, identifiers, contacts)
: "someone";
const object = planClaim.object as GenericVerifiableCredential;
const objectInfo = object ? " " + claimSummary(object) : "";
return contactInfo + " planned" + objectInfo;
} else if (type === "Tenure") {
// party.did is for legacy data, before March 2023
const claimer = claim.party?.identifier || claim.party?.did;
const contactInfo = didInfo(claimer, activeDid, identifiers, contacts);
const polygon = claim.spatialUnit?.geo?.polygon || "";
return (
contactInfo +
" possesses [" +
polygon.substring(0, polygon.indexOf(" ")) +
"...]"
);
const tenureClaim = claim as ClaimObject;
const agent: Agent = tenureClaim.agent || {
identifier: undefined,
did: undefined,
};
const agentDid = agent.did || agent.identifier;
const contactInfo = agentDid
? didInfo(agentDid, activeDid, identifiers, contacts)
: "someone";
const object = tenureClaim.object as GenericVerifiableCredential;
const objectInfo = object ? " " + claimSummary(object) : "";
return contactInfo + " has tenure" + objectInfo;
} else {
return (
issuer +
" declared " +
claimSummary(claim as GenericCredWrapper<GenericVerifiableCredential>)
);
return issuer + " declared " + claimSummary(claim);
}
};
@@ -1288,9 +1325,7 @@ export async function createEndorserJwtVcFromClaim(
export async function createInviteJwt(
activeDid: string,
contact?: Contact,
inviteId?: string,
expiresIn?: number,
contact: Contact,
): Promise<string> {
const vcClaim: RegisterVerifiableCredential = {
"@context": SCHEMA_ORG_CONTEXT,
@@ -1301,19 +1336,19 @@ export async function createInviteJwt(
if (contact) {
vcClaim.participant = { identifier: contact.did };
}
if (inviteId) {
vcClaim.identifier = inviteId;
}
// Make a payload for the claim
const vcPayload = {
const vcPayload: { vc: VerifiableCredentialClaim } = {
vc: {
"@context": ["https://www.w3.org/2018/credentials/v1"],
"@type": "VerifiableCredential",
type: ["VerifiableCredential"],
credentialSubject: vcClaim,
credentialSubject: vcClaim as unknown as ClaimObject, // Type assertion needed due to object being string
},
};
// Create a signature using private key of identity
const vcJwt = await createEndorserJwtForDid(activeDid, vcPayload, expiresIn);
const vcJwt = await createEndorserJwtForDid(activeDid, vcPayload);
return vcJwt;
}
@@ -1323,21 +1358,44 @@ export async function register(
axios: Axios,
contact: Contact,
): Promise<{ success?: boolean; error?: string }> {
const vcJwt = await createInviteJwt(activeDid, contact);
try {
const vcJwt = await createInviteJwt(activeDid, contact);
const url = apiServer + "/api/v2/claim";
const resp = await axios.post<{
success?: {
handleId?: string;
embeddedRecordError?: string;
};
error?: string;
message?: string;
}>(url, { jwtEncoded: vcJwt });
const url = apiServer + "/api/v2/claim";
const resp = await axios.post(url, { jwtEncoded: vcJwt });
if (resp.data?.success?.handleId) {
return { success: true };
} else if (resp.data?.success?.embeddedRecordError) {
let message =
"There was some problem with the registration and so it may not be complete.";
if (typeof resp.data.success.embeddedRecordError == "string") {
message += " " + resp.data.success.embeddedRecordError;
if (resp.data?.success?.handleId) {
return { success: true };
} else if (resp.data?.success?.embeddedRecordError) {
let message =
"There was some problem with the registration and so it may not be complete.";
if (typeof resp.data.success.embeddedRecordError === "string") {
message += " " + resp.data.success.embeddedRecordError;
}
return { error: message };
} else {
logger.error("Registration error:", JSON.stringify(resp.data));
return { error: "Got a server error when registering." };
}
} catch (error: unknown) {
if (error && typeof error === "object") {
const err = error as AxiosErrorResponse;
const errorMessage =
err.message ||
(err.response?.data &&
typeof err.response.data === "object" &&
"message" in err.response.data
? (err.response.data as { message: string }).message
: undefined);
logger.error("Registration error:", errorMessage || JSON.stringify(err));
return { error: errorMessage || "Got a server error when registering." };
}
return { error: message };
} else {
logger.error(resp);
return { error: "Got a server error when registering." };
}
}
@@ -1363,7 +1421,14 @@ export async function setVisibilityUtil(
if (resp.status === 200) {
const success = resp.data.success;
if (success) {
db.contacts.update(contact.did, { seesMe: visibility });
const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec(
"UPDATE contacts SET seesMe = ? WHERE did = ?",
[visibility, contact.did],
);
if (USE_DEXIE_DB) {
db.contacts.update(contact.did, { seesMe: visibility });
}
}
return { success };
} else {

View File

@@ -5,29 +5,43 @@ import { Buffer } from "buffer";
import * as R from "ramda";
import { useClipboard } from "@vueuse/core";
import { DEFAULT_PUSH_SERVER, NotificationIface } from "../constants/app";
import {
DEFAULT_PUSH_SERVER,
NotificationIface,
USE_DEXIE_DB,
} from "../constants/app";
import {
accountsDBPromise,
retrieveSettingsForActiveAccount,
updateAccountSettings,
updateDefaultSettings,
} from "../db/index";
import { Account } from "../db/tables/accounts";
import { Account, AccountEncrypted } from "../db/tables/accounts";
import { Contact } from "../db/tables/contacts";
import * as databaseUtil from "../db/databaseUtil";
import { DEFAULT_PASSKEY_EXPIRATION_MINUTES } from "../db/tables/settings";
import { deriveAddress, generateSeed, newIdentifier } from "../libs/crypto";
import * as serverUtil from "../libs/endorserServer";
import {
containsHiddenDid,
arrayBufferToBase64,
base64ToArrayBuffer,
deriveAddress,
generateSeed,
newIdentifier,
simpleDecrypt,
simpleEncrypt,
} from "../libs/crypto";
import * as serverUtil from "../libs/endorserServer";
import { containsHiddenDid } from "../libs/endorserServer";
import {
GenericCredWrapper,
GenericVerifiableCredential,
GiveSummaryRecord,
OfferVerifiableCredential,
} from "../libs/endorserServer";
import { KeyMeta } from "../libs/crypto/vc";
} from "../interfaces/common";
import { GiveSummaryRecord } from "../interfaces/records";
import { OfferVerifiableCredential } from "../interfaces/claims";
import { KeyMeta } from "../interfaces/common";
import { createPeerDid } from "../libs/crypto/vc/didPeer";
import { registerCredential } from "../libs/crypto/vc/passkeyDidPeer";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
export interface GiverReceiverInputInfo {
did?: string;
@@ -66,18 +80,24 @@ export const UNIT_LONG: Record<string, string> = {
};
/* eslint-enable prettier/prettier */
const UNIT_CODES: Record<string, Record<string, string>> = {
const UNIT_CODES: Record<
string,
{ name: string; faIcon: string; decimals: number }
> = {
BTC: {
name: "Bitcoin",
faIcon: "bitcoin-sign",
decimals: 4,
},
HUR: {
name: "hours",
faIcon: "clock",
decimals: 0,
},
USD: {
name: "US Dollars",
faIcon: "dollar",
decimals: 2,
},
};
@@ -85,6 +105,13 @@ export function iconForUnitCode(unitCode: string) {
return UNIT_CODES[unitCode]?.faIcon || "question";
}
export function formattedAmount(amount: number, unitCode: string) {
const unit = UNIT_CODES[unitCode];
const amountStr = amount.toFixed(unit?.decimals ?? 4);
const unitName = unit?.name || "?";
return amountStr + " " + unitName;
}
// from https://stackoverflow.com/a/175787/845494
// ... though it appears even this isn't precisely right so keep doing "|| 0" or something in sensitive places
//
@@ -364,14 +391,15 @@ export function base64ToBlob(base64DataUrl: string, sliceSize = 512) {
* @param veriClaim is expected to have fields: claim and issuer
*/
export function offerGiverDid(
veriClaim: GenericCredWrapper<OfferVerifiableCredential>,
veriClaim: GenericCredWrapper<GenericVerifiableCredential>,
): string | undefined {
let giver;
if (
veriClaim.claim.offeredBy?.identifier &&
!serverUtil.isHiddenDid(veriClaim.claim.offeredBy.identifier as string)
) {
giver = veriClaim.claim.offeredBy.identifier;
const claim = veriClaim.claim as OfferVerifiableCredential;
const offeredBy: { identifier?: string } | undefined =
claim.offeredBy || claim.credentialSubject?.offeredBy;
const offeredById = offeredBy?.identifier;
if (offeredById && !serverUtil.isHiddenDid(offeredById)) {
giver = offeredById;
} else if (veriClaim.issuer && !serverUtil.isHiddenDid(veriClaim.issuer)) {
giver = veriClaim.issuer;
}
@@ -385,10 +413,7 @@ export function offerGiverDid(
export const canFulfillOffer = (
veriClaim: GenericCredWrapper<GenericVerifiableCredential>,
) => {
return (
veriClaim.claimType === "Offer" &&
!!offerGiverDid(veriClaim as GenericCredWrapper<OfferVerifiableCredential>)
);
return veriClaim.claimType === "Offer" && !!offerGiverDid(veriClaim);
};
// return object with paths and arrays of DIDs for any keys ending in "VisibleToDid"
@@ -457,19 +482,44 @@ export function findAllVisibleToDids(
*
**/
export interface AccountKeyInfo extends Account, KeyMeta {}
export interface AccountKeyInfo
extends Omit<Account, "derivationPath">,
Omit<KeyMeta, "derivationPath"> {
derivationPath?: string; // Make it optional to match Account type
}
export const retrieveAccountCount = async (): Promise<number> => {
// one of the few times we use accountsDBPromise directly; try to avoid more usage
const accountsDB = await accountsDBPromise;
return await accountsDB.accounts.count();
let result = 0;
const platformService = PlatformServiceFactory.getInstance();
const dbResult = await platformService.dbQuery(
`SELECT COUNT(*) FROM accounts`,
);
if (dbResult?.values?.[0]?.[0]) {
result = dbResult.values[0][0] as number;
}
if (USE_DEXIE_DB) {
// one of the few times we use accountsDBPromise directly; try to avoid more usage
const accountsDB = await accountsDBPromise;
result = await accountsDB.accounts.count();
}
return result;
};
export const retrieveAccountDids = async (): Promise<string[]> => {
// one of the few times we use accountsDBPromise directly; try to avoid more usage
const accountsDB = await accountsDBPromise;
const allAccounts = await accountsDB.accounts.toArray();
const allDids = allAccounts.map((acc) => acc.did);
const platformService = PlatformServiceFactory.getInstance();
const dbAccounts = await platformService.dbQuery(`SELECT did FROM accounts`);
let allDids =
databaseUtil
.mapQueryResultToValues(dbAccounts)
?.map((row) => row[0] as string) || [];
if (USE_DEXIE_DB) {
// this is the old way
// one of the few times we use accountsDBPromise directly; try to avoid more usage
const accountsDB = await accountsDBPromise;
const allAccounts = await accountsDB.accounts.toArray();
allDids = allAccounts.map((acc) => acc.did);
}
return allDids;
};
@@ -478,53 +528,188 @@ export const retrieveAccountDids = async (): Promise<string[]> => {
export const retrieveAccountMetadata = async (
activeDid: string,
): Promise<AccountKeyInfo | undefined> => {
// one of the few times we use accountsDBPromise directly; try to avoid more usage
const accountsDB = await accountsDBPromise;
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first()) as Account;
let result: AccountKeyInfo | undefined = undefined;
const platformService = PlatformServiceFactory.getInstance();
const dbAccount = await platformService.dbQuery(
`SELECT * FROM accounts WHERE did = ?`,
[activeDid],
);
const account = databaseUtil.mapQueryResultToValues(dbAccount)[0] as Account;
if (account) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { identity, mnemonic, ...metadata } = account;
return metadata;
result = metadata;
} else {
return undefined;
result = undefined;
}
if (USE_DEXIE_DB) {
// one of the few times we use accountsDBPromise directly; try to avoid more usage
const accountsDB = await accountsDBPromise;
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first()) as Account;
if (account) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { identity, mnemonic, ...metadata } = account;
result = metadata;
} else {
result = undefined;
}
}
return result;
};
export const retrieveAllAccountsMetadata = async (): Promise<Account[]> => {
// one of the few times we use accountsDBPromise directly; try to avoid more usage
const accountsDB = await accountsDBPromise;
const array = await accountsDB.accounts.toArray();
return array.map((account) => {
const platformService = PlatformServiceFactory.getInstance();
const sql = `SELECT * FROM accounts`;
const dbAccounts = await platformService.dbQuery(sql);
const accounts = databaseUtil.mapQueryResultToValues(dbAccounts) as Account[];
let result = accounts.map((account) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { identity, mnemonic, ...metadata } = account;
return metadata;
return metadata as Account;
});
if (USE_DEXIE_DB) {
// one of the few times we use accountsDBPromise directly; try to avoid more usage
const accountsDB = await accountsDBPromise;
const array = await accountsDB.accounts.toArray();
result = array.map((account) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { identity, mnemonic, ...metadata } = account;
return metadata as Account;
});
}
return result;
};
export const retrieveFullyDecryptedAccount = async (
activeDid: string,
): Promise<AccountKeyInfo | undefined> => {
// one of the few times we use accountsDBPromise directly; try to avoid more usage
const accountsDB = await accountsDBPromise;
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first()) as Account;
return account;
let result: AccountKeyInfo | undefined = undefined;
const platformService = PlatformServiceFactory.getInstance();
const dbSecrets = await platformService.dbQuery(
`SELECT secretBase64 from secret`,
);
if (
!dbSecrets ||
dbSecrets.values.length === 0 ||
dbSecrets.values[0].length === 0
) {
throw new Error(
"No secret found. We recommend you clear your data and start over.",
);
}
const secretBase64 = dbSecrets.values[0][0] as string;
const secret = base64ToArrayBuffer(secretBase64);
const dbAccount = await platformService.dbQuery(
`SELECT * FROM accounts WHERE did = ?`,
[activeDid],
);
if (
!dbAccount ||
dbAccount.values.length === 0 ||
dbAccount.values[0].length === 0
) {
throw new Error("Account not found.");
}
const fullAccountData = databaseUtil.mapQueryResultToValues(
dbAccount,
)[0] as AccountEncrypted;
const identityEncr = base64ToArrayBuffer(fullAccountData.identityEncrBase64);
const mnemonicEncr = base64ToArrayBuffer(fullAccountData.mnemonicEncrBase64);
fullAccountData.identity = await simpleDecrypt(identityEncr, secret);
fullAccountData.mnemonic = await simpleDecrypt(mnemonicEncr, secret);
result = fullAccountData;
if (USE_DEXIE_DB) {
// one of the few times we use accountsDBPromise directly; try to avoid more usage
const accountsDB = await accountsDBPromise;
const account = (await accountsDB.accounts
.where("did")
.equals(activeDid)
.first()) as Account;
result = account;
}
return result;
};
// let's try and eliminate this
export const retrieveAllFullyDecryptedAccounts = async (): Promise<
Array<AccountKeyInfo>
Array<AccountEncrypted>
> => {
const accountsDB = await accountsDBPromise;
const allAccounts = await accountsDB.accounts.toArray();
const platformService = PlatformServiceFactory.getInstance();
const queryResult = await platformService.dbQuery("SELECT * FROM accounts");
let allAccounts = databaseUtil.mapQueryResultToValues(
queryResult,
) as unknown as AccountEncrypted[];
if (USE_DEXIE_DB) {
const accountsDB = await accountsDBPromise;
allAccounts = (await accountsDB.accounts.toArray()) as AccountEncrypted[];
}
return allAccounts;
};
/**
* Saves a new identity to both SQL and Dexie databases
*/
export async function saveNewIdentity(
identity: string,
mnemonic: string,
newId: { did: string; keys: Array<{ publicKeyHex: string }> },
derivationPath: string,
): Promise<void> {
try {
// add to the new sql db
const platformService = PlatformServiceFactory.getInstance();
const secrets = await platformService.dbQuery(
`SELECT secretBase64 FROM secret`,
);
if (!secrets?.values?.length || !secrets.values[0]?.length) {
throw new Error(
"No initial encryption supported. We recommend you clear your data and start over.",
);
}
const secretBase64 = secrets.values[0][0] as string;
const secret = base64ToArrayBuffer(secretBase64);
const encryptedIdentity = await simpleEncrypt(identity, secret);
const encryptedMnemonic = await simpleEncrypt(mnemonic, secret);
const encryptedIdentityBase64 = arrayBufferToBase64(encryptedIdentity);
const encryptedMnemonicBase64 = arrayBufferToBase64(encryptedMnemonic);
const sql = `INSERT INTO accounts (dateCreated, derivationPath, did, identityEncrBase64, mnemonicEncrBase64, publicKeyHex)
VALUES (?, ?, ?, ?, ?, ?)`;
const params = [
new Date().toISOString(),
derivationPath,
newId.did,
encryptedIdentityBase64,
encryptedMnemonicBase64,
newId.keys[0].publicKeyHex,
];
await platformService.dbExec(sql, params);
await databaseUtil.updateDefaultSettings({ activeDid: newId.did });
if (USE_DEXIE_DB) {
// one of the few times we use accountsDBPromise directly; try to avoid more usage
const accountsDB = await accountsDBPromise;
await accountsDB.accounts.add({
dateCreated: new Date().toISOString(),
derivationPath: derivationPath,
did: newId.did,
identity: identity,
mnemonic: mnemonic,
publicKeyHex: newId.keys[0].publicKeyHex,
});
await updateDefaultSettings({ activeDid: newId.did });
}
} catch (error) {
logger.error("Failed to update default settings:", error);
throw new Error(
"Failed to set default settings. Please try again or restart the app.",
);
}
}
/**
* Generates a new identity, saves it to the database, and sets it as the active identity.
* @return {Promise<string>} with the DID of the new identity
@@ -538,26 +723,11 @@ export const generateSaveAndActivateIdentity = async (): Promise<string> => {
const newId = newIdentifier(address, publicHex, privateHex, derivationPath);
const identity = JSON.stringify(newId);
// one of the few times we use accountsDBPromise directly; try to avoid more usage
try {
const accountsDB = await accountsDBPromise;
await accountsDB.accounts.add({
dateCreated: new Date().toISOString(),
derivationPath: derivationPath,
did: newId.did,
identity: identity,
mnemonic: mnemonic,
publicKeyHex: newId.keys[0].publicKeyHex,
});
await updateDefaultSettings({ activeDid: newId.did });
} catch (error) {
console.error("Failed to update default settings:", error);
throw new Error(
"Failed to set default settings. Please try again or restart the app.",
);
await saveNewIdentity(identity, mnemonic, newId, derivationPath);
await databaseUtil.updateAccountSettings(newId.did, { isRegistered: false });
if (USE_DEXIE_DB) {
await updateAccountSettings(newId.did, { isRegistered: false });
}
await updateAccountSettings(newId.did, { isRegistered: false });
return newId.did;
};
@@ -575,9 +745,19 @@ export const registerAndSavePasskey = async (
passkeyCredIdHex,
publicKeyHex: Buffer.from(publicKeyBytes).toString("hex"),
};
// one of the few times we use accountsDBPromise directly; try to avoid more usage
const accountsDB = await accountsDBPromise;
await accountsDB.accounts.add(account);
const insertStatement = databaseUtil.generateInsertStatement(
account,
"accounts",
);
await PlatformServiceFactory.getInstance().dbExec(
insertStatement.sql,
insertStatement.params,
);
if (USE_DEXIE_DB) {
// one of the few times we use accountsDBPromise directly; try to avoid more usage
const accountsDB = await accountsDBPromise;
await accountsDB.accounts.add(account);
}
return account;
};
@@ -585,13 +765,22 @@ export const registerSaveAndActivatePasskey = async (
keyName: string,
): Promise<Account> => {
const account = await registerAndSavePasskey(keyName);
await updateDefaultSettings({ activeDid: account.did });
await updateAccountSettings(account.did, { isRegistered: false });
await databaseUtil.updateDefaultSettings({ activeDid: account.did });
await databaseUtil.updateAccountSettings(account.did, {
isRegistered: false,
});
if (USE_DEXIE_DB) {
await updateDefaultSettings({ activeDid: account.did });
await updateAccountSettings(account.did, { isRegistered: false });
}
return account;
};
export const getPasskeyExpirationSeconds = async (): Promise<number> => {
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
return (
(settings?.passkeyExpirationMinutes ?? DEFAULT_PASSKEY_EXPIRATION_MINUTES) *
60
@@ -607,7 +796,10 @@ export const sendTestThroughPushServer = async (
subscriptionJSON: PushSubscriptionJSON,
skipFilter: boolean,
): Promise<AxiosResponse> => {
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
let pushUrl: string = DEFAULT_PUSH_SERVER as string;
if (settings?.webPushServer) {
pushUrl = settings.webPushServer;
@@ -635,3 +827,96 @@ export const sendTestThroughPushServer = async (
logger.log("Got response from web push server:", response);
return response;
};
/**
* Converts a Contact object to a CSV line string following the established format.
* The format matches CONTACT_CSV_HEADER: "name,did,pubKeyBase64,seesMe,registered,contactMethods"
* where contactMethods is stored as a stringified JSON array.
*
* @param contact - The Contact object to convert
* @returns A CSV-formatted string representing the contact
* @throws {Error} If the contact object is missing required fields
*/
export const contactToCsvLine = (contact: Contact): string => {
if (!contact.did) {
throw new Error("Contact must have a did field");
}
// Escape fields that might contain commas or quotes
const escapeField = (field: string | boolean | undefined): string => {
if (field === undefined) return "";
const str = String(field);
if (str.includes(",") || str.includes('"')) {
return `"${str.replace(/"/g, '""')}"`;
}
return str;
};
// Handle contactMethods array by stringifying it
const contactMethodsStr = contact.contactMethods
? escapeField(JSON.stringify(contact.contactMethods))
: "";
const fields = [
escapeField(contact.name),
escapeField(contact.did),
escapeField(contact.publicKeyBase64),
escapeField(contact.seesMe),
escapeField(contact.registered),
contactMethodsStr,
];
return fields.join(",");
};
/**
* Interface for the JSON export format of database tables
*/
export interface TableExportData {
tableName: string;
rows: Array<Record<string, unknown>>;
}
/**
* Interface for the complete database export format
*/
export interface DatabaseExport {
data: {
data: Array<TableExportData>;
};
}
/**
* Converts an array of contacts to the standardized database export JSON format.
* This format is used for data migration and backup purposes.
*
* @param contacts - Array of Contact objects to convert
* @returns DatabaseExport object in the standardized format
*/
export const contactsToExportJson = (contacts: Contact[]): DatabaseExport => {
// Convert each contact to a plain object and ensure all fields are included
const rows = contacts.map((contact) => ({
did: contact.did,
name: contact.name || null,
contactMethods: contact.contactMethods
? JSON.stringify(contact.contactMethods)
: null,
nextPubKeyHashB64: contact.nextPubKeyHashB64 || null,
notes: contact.notes || null,
profileImageUrl: contact.profileImageUrl || null,
publicKeyBase64: contact.publicKeyBase64 || null,
seesMe: contact.seesMe || false,
registered: contact.registered || false,
}));
return {
data: {
data: [
{
tableName: "contacts",
rows,
},
],
},
};
};

View File

@@ -34,7 +34,7 @@ import router from "./router";
import { handleApiError } from "./services/api";
import { AxiosError } from "axios";
import { DeepLinkHandler } from "./services/deepLinks";
import { logConsoleAndDb } from "./db";
import { logConsoleAndDb } from "./db/databaseUtil";
import { logger } from "./utils/logger";
logger.log("[Capacitor] Starting initialization");

View File

@@ -10,6 +10,12 @@ import { FontAwesomeIcon } from "./libs/fontawesome";
import Camera from "simple-vue-camera";
import { logger } from "./utils/logger";
const platform = process.env.VITE_PLATFORM;
const pwa_enabled = process.env.VITE_PWA_ENABLED === "true";
logger.log("Platform", JSON.stringify({ platform }));
logger.log("PWA enabled", JSON.stringify({ pwa_enabled }));
// Global Error Handler
function setupGlobalErrorHandler(app: VueApp) {
logger.log("[App Init] Setting up global error handler");

View File

@@ -1,4 +1,16 @@
import { initializeApp } from "./main.common";
import { logger } from "./utils/logger";
const platform = process.env.VITE_PLATFORM;
const pwa_enabled = process.env.VITE_PWA_ENABLED === "true";
logger.info("[Electron] Initializing app");
logger.info("[Electron] Platform:", { platform });
logger.info("[Electron] PWA enabled:", { pwa_enabled });
if (pwa_enabled) {
logger.warn("[Electron] PWA is enabled, but not supported in electron");
}
const app = initializeApp();
app.mount("#app");

View File

@@ -1,215 +0,0 @@
import { createPinia } from "pinia";
import { App as VueApp, ComponentPublicInstance, createApp } from "vue";
import App from "./App.vue";
import "./registerServiceWorker";
import router from "./router";
import axios from "axios";
import VueAxios from "vue-axios";
import Notifications from "notiwind";
import "./assets/styles/tailwind.css";
import { library } from "@fortawesome/fontawesome-svg-core";
import {
faArrowDown,
faArrowLeft,
faArrowRight,
faArrowRotateBackward,
faArrowUpRightFromSquare,
faArrowUp,
faBan,
faBitcoinSign,
faBurst,
faCalendar,
faCamera,
faCameraRotate,
faCaretDown,
faChair,
faCheck,
faChevronDown,
faChevronLeft,
faChevronRight,
faChevronUp,
faCircle,
faCircleCheck,
faCircleInfo,
faCircleQuestion,
faCircleUser,
faClock,
faCoins,
faComment,
faCopy,
faDollar,
faEllipsis,
faEllipsisVertical,
faEnvelopeOpenText,
faEraser,
faEye,
faEyeSlash,
faFileContract,
faFileLines,
faFilter,
faFloppyDisk,
faFolderOpen,
faForward,
faGift,
faGlobe,
faHammer,
faHand,
faHandHoldingDollar,
faHandHoldingHeart,
faHouseChimney,
faImage,
faImagePortrait,
faLeftRight,
faLightbulb,
faLink,
faLocationDot,
faLongArrowAltLeft,
faLongArrowAltRight,
faMagnifyingGlass,
faMessage,
faMinus,
faPen,
faPersonCircleCheck,
faPersonCircleQuestion,
faPlus,
faQuestion,
faQrcode,
faRightFromBracket,
faRotate,
faShareNodes,
faSpinner,
faSquare,
faSquareCaretDown,
faSquareCaretUp,
faSquarePlus,
faTrashCan,
faTriangleExclamation,
faUser,
faUsers,
faXmark,
} from "@fortawesome/free-solid-svg-icons";
library.add(
faArrowDown,
faArrowLeft,
faArrowRight,
faArrowRotateBackward,
faArrowUpRightFromSquare,
faArrowUp,
faBan,
faBitcoinSign,
faBurst,
faCalendar,
faCamera,
faCameraRotate,
faCaretDown,
faChair,
faCheck,
faChevronDown,
faChevronLeft,
faChevronRight,
faChevronUp,
faCircle,
faCircleCheck,
faCircleInfo,
faCircleQuestion,
faCircleUser,
faClock,
faCoins,
faComment,
faCopy,
faDollar,
faEllipsis,
faEllipsisVertical,
faEnvelopeOpenText,
faEraser,
faEye,
faEyeSlash,
faFileContract,
faFileLines,
faFilter,
faFloppyDisk,
faFolderOpen,
faForward,
faGift,
faGlobe,
faHammer,
faHand,
faHandHoldingDollar,
faHandHoldingHeart,
faHouseChimney,
faImage,
faImagePortrait,
faLeftRight,
faLightbulb,
faLink,
faLocationDot,
faLongArrowAltLeft,
faLongArrowAltRight,
faMagnifyingGlass,
faMessage,
faMinus,
faPen,
faPersonCircleCheck,
faPersonCircleQuestion,
faPlus,
faQrcode,
faQuestion,
faRotate,
faRightFromBracket,
faShareNodes,
faSpinner,
faSquare,
faSquareCaretDown,
faSquareCaretUp,
faSquarePlus,
faTrashCan,
faTriangleExclamation,
faUser,
faUsers,
faXmark,
);
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import Camera from "simple-vue-camera";
import { logger } from "./utils/logger";
// Can trigger this with a 'throw' inside some top-level function, eg. on the HomeView
function setupGlobalErrorHandler(app: VueApp) {
// @ts-expect-error 'cause we cannot see why config is not defined
app.config.errorHandler = (
err: Error,
instance: ComponentPublicInstance | null,
info: string,
) => {
logger.error(
"Ouch! Global Error Handler.",
"Error:",
err,
"- Error toString:",
err.toString(),
"- Info:",
info,
"- Instance:",
instance,
);
// Want to show a nice notiwind notification but can't figure out how.
alert(
(err.message || "Something bad happened") +
" - Try reloading or restarting the app.",
);
};
}
const app = createApp(App)
.component("fa", FontAwesomeIcon)
.component("camera", Camera)
.use(createPinia())
.use(VueAxios, axios)
.use(router)
.use(Notifications);
setupGlobalErrorHandler(app);
app.mount("#app");

View File

@@ -1,5 +1,37 @@
import { initBackend } from "absurd-sql/dist/indexeddb-main-thread";
import { initializeApp } from "./main.common";
import "./registerServiceWorker"; // Web PWA support
import { logger } from "./utils/logger";
const platform = process.env.VITE_PLATFORM;
const pwa_enabled = process.env.VITE_PWA_ENABLED === "true";
logger.info("[Web] PWA enabled", { pwa_enabled });
logger.info("[Web] Platform", { platform });
// Only import service worker for web builds
if (platform !== "electron" && pwa_enabled) {
import("./registerServiceWorker"); // Web PWA support
}
const app = initializeApp();
function sqlInit() {
// see https://github.com/jlongster/absurd-sql
const worker = new Worker(
new URL("./registerSQLWorker.js", import.meta.url),
{
type: "module",
},
);
// This is only required because Safari doesn't support nested
// workers. This installs a handler that will proxy creating web
// workers through the main thread
initBackend(worker);
}
if (platform === "web" || platform === "development") {
sqlInit();
} else {
logger.info("[Web] SQL not initialized for platform", { platform });
}
app.mount("#app");

6
src/registerSQLWorker.js Normal file
View File

@@ -0,0 +1,6 @@
import databaseService from "./services/AbsurdSqlDatabaseService";
async function run() {
await databaseService.initialize();
}
run();

View File

@@ -2,8 +2,18 @@
import { register } from "register-service-worker";
// Only register service worker if explicitly enabled and in production
// Check if we're in an Electron environment
const isElectron =
process.env.VITE_PLATFORM === "electron" ||
process.env.VITE_DISABLE_PWA === "true" ||
window.navigator.userAgent.toLowerCase().includes("electron");
// Only register service worker if:
// 1. Not in Electron
// 2. PWA is explicitly enabled
// 3. In production mode
if (
!isElectron &&
process.env.VITE_PWA_ENABLED === "true" &&
process.env.NODE_ENV === "production"
) {
@@ -34,6 +44,12 @@ if (
});
} else {
console.log(
"Service worker registration skipped - not enabled or not in production",
`Service worker registration skipped - ${
isElectron
? "running in Electron"
: process.env.VITE_PWA_ENABLED !== "true"
? "PWA not enabled"
: "not in production mode"
}`,
);
}

View File

@@ -2,35 +2,11 @@ import {
createRouter,
createWebHistory,
createMemoryHistory,
NavigationGuardNext,
RouteLocationNormalized,
RouteRecordRaw,
} from "vue-router";
import { accountsDBPromise } from "../db/index";
import { logger } from "../utils/logger";
/**
*
* @param to :RouteLocationNormalized
* @param from :RouteLocationNormalized
* @param next :NavigationGuardNext
*/
const enterOrStart = async (
to: RouteLocationNormalized,
from: RouteLocationNormalized,
next: NavigationGuardNext,
) => {
// one of the few times we use accountsDBPromise directly; try to avoid more usage
const accountsDB = await accountsDBPromise;
const num_accounts = await accountsDB.accounts.count();
if (num_accounts > 0) {
next();
} else {
next({ name: "start" });
}
};
const routes: Array<RouteRecordRaw> = [
{
path: "/account",
@@ -216,7 +192,6 @@ const routes: Array<RouteRecordRaw> = [
path: "/projects",
name: "projects",
component: () => import("../views/ProjectsView.vue"),
beforeEnter: enterOrStart,
},
{
path: "/quick-action-bvc",

View File

@@ -0,0 +1,29 @@
import { DatabaseService } from "../interfaces/database";
declare module "@jlongster/sql.js" {
interface SQL {
Database: unknown;
FS: unknown;
register_for_idb: (fs: unknown) => void;
}
function initSqlJs(config: {
locateFile: (file: string) => string;
}): Promise<SQL>;
export default initSqlJs;
}
declare module "absurd-sql" {
export class SQLiteFS {
constructor(fs: unknown, backend: unknown);
}
}
declare module "absurd-sql/dist/indexeddb-backend" {
export default class IndexedDBBackend {
constructor();
}
}
declare const databaseService: DatabaseService;
export default databaseService;

View File

@@ -0,0 +1,231 @@
import initSqlJs from "@jlongster/sql.js";
import { SQLiteFS } from "absurd-sql";
import IndexedDBBackend from "absurd-sql/dist/indexeddb-backend";
import { runMigrations } from "../db-sql/migration";
import type { DatabaseService, QueryExecResult } from "../interfaces/database";
import { logger } from "@/utils/logger";
interface QueuedOperation {
type: "run" | "query";
sql: string;
params: unknown[];
resolve: (value: unknown) => void;
reject: (reason: unknown) => void;
}
interface AbsurdSqlDatabase {
exec: (sql: string, params?: unknown[]) => Promise<QueryExecResult[]>;
run: (
sql: string,
params?: unknown[],
) => Promise<{ changes: number; lastId?: number }>;
}
class AbsurdSqlDatabaseService implements DatabaseService {
private static instance: AbsurdSqlDatabaseService | null = null;
private db: AbsurdSqlDatabase | null;
private initialized: boolean;
private initializationPromise: Promise<void> | null = null;
private operationQueue: Array<QueuedOperation> = [];
private isProcessingQueue: boolean = false;
private constructor() {
this.db = null;
this.initialized = false;
}
static getInstance(): AbsurdSqlDatabaseService {
if (!AbsurdSqlDatabaseService.instance) {
AbsurdSqlDatabaseService.instance = new AbsurdSqlDatabaseService();
}
return AbsurdSqlDatabaseService.instance;
}
async initialize(): Promise<void> {
// If already initialized, return immediately
if (this.initialized) {
return;
}
// If initialization is in progress, wait for it
if (this.initializationPromise) {
return this.initializationPromise;
}
// Start initialization
this.initializationPromise = this._initialize();
try {
await this.initializationPromise;
} catch (error) {
logger.error(`AbsurdSqlDatabaseService initialize method failed:`, error);
this.initializationPromise = null; // Reset on failure
throw error;
}
}
private async _initialize(): Promise<void> {
if (this.initialized) {
return;
}
const SQL = await initSqlJs({
locateFile: (file: string) => {
return new URL(
`/node_modules/@jlongster/sql.js/dist/${file}`,
import.meta.url,
).href;
},
});
const sqlFS = new SQLiteFS(SQL.FS, new IndexedDBBackend());
SQL.register_for_idb(sqlFS);
SQL.FS.mkdir("/sql");
SQL.FS.mount(sqlFS, {}, "/sql");
const path = "/sql/timesafari.absurd-sql";
if (typeof SharedArrayBuffer === "undefined") {
const stream = SQL.FS.open(path, "a+");
await stream.node.contents.readIfFallback();
SQL.FS.close(stream);
}
this.db = new SQL.Database(path, { filename: true });
if (!this.db) {
throw new Error(
"The database initialization failed. We recommend you restart or reinstall.",
);
}
// An error is thrown without this pragma: "File has invalid page size. (the first block of a new file must be written first)"
await this.db.exec(`PRAGMA journal_mode=MEMORY;`);
const sqlExec = this.db.run.bind(this.db);
const sqlQuery = this.db.exec.bind(this.db);
// Extract the migration names for the absurd-sql format
const extractMigrationNames: (result: QueryExecResult[]) => Set<string> = (
result,
) => {
// Even with the "select name" query, the QueryExecResult may be [] (which doesn't make sense to me).
const names = result?.[0]?.values.map((row) => row[0] as string) || [];
return new Set(names);
};
// Run migrations
await runMigrations(sqlExec, sqlQuery, extractMigrationNames);
this.initialized = true;
// Start processing the queue after initialization
this.processQueue();
}
private async processQueue(): Promise<void> {
if (this.isProcessingQueue || !this.initialized || !this.db) {
return;
}
this.isProcessingQueue = true;
while (this.operationQueue.length > 0) {
const operation = this.operationQueue.shift();
if (!operation) continue;
try {
let result: unknown;
switch (operation.type) {
case "run":
result = await this.db.run(operation.sql, operation.params);
break;
case "query":
result = await this.db.exec(operation.sql, operation.params);
break;
}
operation.resolve(result);
} catch (error) {
logger.error(
"Error while processing SQL queue:",
error,
" ... for sql:",
operation.sql,
" ... with params:",
operation.params,
);
operation.reject(error);
}
}
this.isProcessingQueue = false;
}
private async queueOperation<R>(
type: QueuedOperation["type"],
sql: string,
params: unknown[] = [],
): Promise<R> {
return new Promise<R>((resolve, reject) => {
const operation: QueuedOperation = {
type,
sql,
params,
resolve: (value: unknown) => resolve(value as R),
reject,
};
this.operationQueue.push(operation);
// If we're already initialized, start processing the queue
if (this.initialized && this.db) {
this.processQueue();
}
});
}
private async waitForInitialization(): Promise<void> {
// If we have an initialization promise, wait for it
if (this.initializationPromise) {
await this.initializationPromise;
return;
}
// If not initialized and no promise, start initialization
if (!this.initialized) {
await this.initialize();
return;
}
// If initialized but no db, something went wrong
if (!this.db) {
logger.error(
`Database not properly initialized after await waitForInitialization() - initialized flag is true but db is null`,
);
throw new Error(
`The database could not be initialized. We recommend you restart or reinstall.`,
);
}
}
// Used for inserts, updates, and deletes
async run(
sql: string,
params: unknown[] = [],
): Promise<{ changes: number; lastId?: number }> {
await this.waitForInitialization();
return this.queueOperation<{ changes: number; lastId?: number }>(
"run",
sql,
params,
);
}
// Note that the resulting array may be empty if there are no results from the query
async query(sql: string, params: unknown[] = []): Promise<QueryExecResult[]> {
await this.waitForInitialization();
return this.queueOperation<QueryExecResult[]>("query", sql, params);
}
}
// Create a singleton instance
const databaseService = AbsurdSqlDatabaseService.getInstance();
export default databaseService;

View File

@@ -1,3 +1,5 @@
import { QueryExecResult } from "@/interfaces/database";
/**
* Represents the result of an image capture or selection operation.
* Contains both the image data as a Blob and the associated filename.
@@ -106,4 +108,26 @@ export interface PlatformService {
* @returns Promise that resolves when the deep link has been handled
*/
handleDeepLink(url: string): Promise<void>;
/**
* Executes a SQL query on the database.
* @param sql - The SQL query to execute
* @param params - The parameters to pass to the query
* @returns Promise resolving to the query result
*/
dbQuery(
sql: string,
params?: unknown[],
): Promise<QueryExecResult | undefined>;
/**
* Executes a create/update/delete on the database.
* @param sql - The SQL statement to execute
* @param params - The parameters to pass to the statement
* @returns Promise resolving to the result of the statement
*/
dbExec(
sql: string,
params?: unknown[],
): Promise<{ changes: number; lastId?: number }>;
}

View File

@@ -4,7 +4,13 @@ import {
StartScanOptions,
LensFacing,
} from "@capacitor-mlkit/barcode-scanning";
import { QRScannerService, ScanListener, QRScannerOptions } from "./types";
import {
QRScannerService,
ScanListener,
QRScannerOptions,
CameraStateListener,
CameraState,
} from "./types";
import { logger } from "@/utils/logger";
export class CapacitorQRScanner implements QRScannerService {
@@ -12,6 +18,9 @@ export class CapacitorQRScanner implements QRScannerService {
private isScanning = false;
private listenerHandles: Array<() => Promise<void>> = [];
private cleanupPromise: Promise<void> | null = null;
private cameraStateListeners: Set<CameraStateListener> = new Set();
private currentState: CameraState = "off";
private currentStateMessage?: string;
async checkPermissions(): Promise<boolean> {
try {
@@ -79,8 +88,11 @@ export class CapacitorQRScanner implements QRScannerService {
}
try {
this.updateCameraState("initializing", "Starting camera...");
// Ensure we have permissions before starting
if (!(await this.checkPermissions())) {
this.updateCameraState("permission_denied", "Camera permission denied");
logger.debug("Requesting camera permissions");
const granted = await this.requestPermissions();
if (!granted) {
@@ -90,11 +102,16 @@ export class CapacitorQRScanner implements QRScannerService {
// Check if scanning is supported
if (!(await this.isSupported())) {
this.updateCameraState(
"error",
"QR scanning not supported on this device",
);
throw new Error("QR scanning not supported on this device");
}
logger.info("Starting MLKit scanner");
this.isScanning = true;
this.updateCameraState("active", "Camera is active");
const scanOptions: StartScanOptions = {
formats: [BarcodeFormat.QrCode],
@@ -126,6 +143,7 @@ export class CapacitorQRScanner implements QRScannerService {
stack: wrappedError.stack,
});
this.isScanning = false;
this.updateCameraState("error", wrappedError.message);
await this.cleanup();
this.scanListener?.onError?.(wrappedError);
throw wrappedError;
@@ -140,6 +158,7 @@ export class CapacitorQRScanner implements QRScannerService {
try {
logger.debug("Stopping QR scanner");
this.updateCameraState("off", "Camera stopped");
await BarcodeScanner.stopScan();
logger.info("QR scanner stopped successfully");
} catch (error) {
@@ -149,6 +168,7 @@ export class CapacitorQRScanner implements QRScannerService {
error: wrappedError.message,
stack: wrappedError.stack,
});
this.updateCameraState("error", wrappedError.message);
this.scanListener?.onError?.(wrappedError);
throw wrappedError;
} finally {
@@ -207,4 +227,23 @@ export class CapacitorQRScanner implements QRScannerService {
// No-op for native scanner
callback(null);
}
addCameraStateListener(listener: CameraStateListener): void {
this.cameraStateListeners.add(listener);
// Immediately notify the new listener of current state
listener.onStateChange(this.currentState, this.currentStateMessage);
}
removeCameraStateListener(listener: CameraStateListener): void {
this.cameraStateListeners.delete(listener);
}
private updateCameraState(state: CameraState, message?: string): void {
this.currentState = state;
this.currentStateMessage = message;
// Notify all listeners of state change
for (const listener of this.cameraStateListeners) {
listener.onStateChange(state, message);
}
}
}

View File

@@ -30,14 +30,16 @@ export class WebInlineQRScanner implements QRScannerService {
private cameraStateListeners: Set<CameraStateListener> = new Set();
private currentState: CameraState = "off";
private currentStateMessage?: string;
private options: QRScannerOptions;
constructor(private options?: QRScannerOptions) {
constructor(options?: QRScannerOptions) {
// Generate a short random ID for this scanner instance
this.id = Math.random().toString(36).substring(2, 8).toUpperCase();
this.options = options ?? {};
logger.error(
`[WebInlineQRScanner:${this.id}] Initializing scanner with options:`,
{
...options,
...this.options,
buildId: BUILD_ID,
targetFps: this.TARGET_FPS,
},
@@ -494,26 +496,34 @@ export class WebInlineQRScanner implements QRScannerService {
}
}
async startScan(): Promise<void> {
async startScan(options?: QRScannerOptions): Promise<void> {
if (this.isScanning) {
logger.error(`[WebInlineQRScanner:${this.id}] Scanner already running`);
return;
}
// Update options if provided
if (options) {
this.options = { ...this.options, ...options };
}
try {
this.isScanning = true;
this.scanAttempts = 0;
this.lastScanTime = Date.now();
this.updateCameraState("initializing", "Starting camera...");
logger.error(`[WebInlineQRScanner:${this.id}] Starting scan`);
logger.error(
`[WebInlineQRScanner:${this.id}] Starting scan with options:`,
this.options,
);
// Get camera stream
// Get camera stream with options
logger.error(
`[WebInlineQRScanner:${this.id}] Requesting camera stream...`,
);
this.stream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: "environment",
facingMode: this.options.camera === "front" ? "user" : "environment",
width: { ideal: 1280 },
height: { ideal: 720 },
},
@@ -527,11 +537,18 @@ export class WebInlineQRScanner implements QRScannerService {
label: t.label,
readyState: t.readyState,
})),
options: this.options,
});
// Set up video element
if (this.video) {
this.video.srcObject = this.stream;
// Only show preview if showPreview is true
if (this.options.showPreview) {
this.video.style.display = "block";
} else {
this.video.style.display = "none";
}
await this.video.play();
logger.error(
`[WebInlineQRScanner:${this.id}] Video element started playing`,

View File

@@ -52,7 +52,7 @@ import {
routeSchema,
DeepLinkRoute,
} from "../interfaces/deepLinks";
import { logConsoleAndDb } from "../db";
import { logConsoleAndDb } from "../db/databaseUtil";
import type { DeepLinkError } from "../interfaces/deepLinks";
/**
@@ -119,6 +119,15 @@ export class DeepLinkHandler {
const [path, queryString] = parts[1].split("?");
const [routePath, param] = path.split("/");
// Validate route exists before proceeding
if (!this.ROUTE_MAP[routePath]) {
throw {
code: "INVALID_ROUTE",
message: `Invalid route path: ${routePath}`,
details: { routePath },
};
}
const query: Record<string, string> = {};
if (queryString) {
new URLSearchParams(queryString).forEach((value, key) => {
@@ -128,11 +137,9 @@ export class DeepLinkHandler {
const params: Record<string, string> = {};
if (param) {
if (this.ROUTE_MAP[routePath].paramKey) {
params[this.ROUTE_MAP[routePath].paramKey] = param;
} else {
params["id"] = param;
}
// Now we know routePath exists in ROUTE_MAP
const routeConfig = this.ROUTE_MAP[routePath];
params[routeConfig.paramKey ?? "id"] = param;
}
return { path: routePath, params, query };
}

View File

@@ -0,0 +1,60 @@
interface Migration {
name: string;
sql: string;
}
export class MigrationService {
private static instance: MigrationService;
private migrations: Migration[] = [];
private constructor() {}
static getInstance(): MigrationService {
if (!MigrationService.instance) {
MigrationService.instance = new MigrationService();
}
return MigrationService.instance;
}
registerMigration(migration: Migration) {
this.migrations.push(migration);
}
/**
* @param sqlExec - A function that executes a SQL statement and returns some update result
* @param sqlQuery - A function that executes a SQL query and returns the result in some format
* @param extractMigrationNames - A function that extracts the names (string array) from a "select name from migrations" query
*/
async runMigrations<T>(
// note that this does not take parameters because the Capacitor SQLite 'execute' is different
sqlExec: (sql: string) => Promise<unknown>,
sqlQuery: (sql: string) => Promise<T>,
extractMigrationNames: (result: T) => Set<string>,
): Promise<void> {
// Create migrations table if it doesn't exist
await sqlExec(`
CREATE TABLE IF NOT EXISTS migrations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
`);
// Get list of executed migrations
const result1: T = await sqlQuery("SELECT name FROM migrations;");
const executedMigrations = extractMigrationNames(result1);
// Run pending migrations in order
for (const migration of this.migrations) {
if (!executedMigrations.has(migration.name)) {
await sqlExec(migration.sql);
await sqlExec(
`INSERT INTO migrations (name) VALUES ('${migration.name}')`,
);
}
}
}
}
export default MigrationService.getInstance();

View File

@@ -1,23 +1,237 @@
import { Filesystem, Directory, Encoding } from "@capacitor/filesystem";
import { Camera, CameraResultType, CameraSource, CameraDirection } from "@capacitor/camera";
import { Share } from "@capacitor/share";
import {
SQLiteConnection,
SQLiteDBConnection,
CapacitorSQLite,
capSQLiteChanges,
DBSQLiteValues,
} from "@capacitor-community/sqlite";
import { runMigrations } from "@/db-sql/migration";
import { QueryExecResult } from "@/interfaces/database";
import {
ImageResult,
PlatformService,
PlatformCapabilities,
} from "../PlatformService";
import { Filesystem, Directory, Encoding } from "@capacitor/filesystem";
import { Camera, CameraResultType, CameraSource, CameraDirection } from "@capacitor/camera";
import { Share } from "@capacitor/share";
import { logger } from "../../utils/logger";
interface QueuedOperation {
type: "run" | "query";
sql: string;
params: unknown[];
resolve: (value: unknown) => void;
reject: (reason: unknown) => void;
}
/**
* Platform service implementation for Capacitor (mobile) platform.
* Provides native mobile functionality through Capacitor plugins for:
* - File system operations
* - Camera and image picker
* - Platform-specific features
* - SQLite database operations
*/
export class CapacitorPlatformService implements PlatformService {
/** Current camera direction */
private currentDirection: CameraDirection = 'BACK';
private currentDirection: CameraDirection = "BACK";
private sqlite: SQLiteConnection;
private db: SQLiteDBConnection | null = null;
private dbName = "timesafari.sqlite";
private initialized = false;
private initializationPromise: Promise<void> | null = null;
private operationQueue: Array<QueuedOperation> = [];
private isProcessingQueue: boolean = false;
constructor() {
this.sqlite = new SQLiteConnection(CapacitorSQLite);
}
private async initializeDatabase(): Promise<void> {
// If already initialized, return immediately
if (this.initialized) {
return;
}
// If initialization is in progress, wait for it
if (this.initializationPromise) {
return this.initializationPromise;
}
// Start initialization
this.initializationPromise = this._initialize();
try {
await this.initializationPromise;
} catch (error) {
logger.error(
"[CapacitorPlatformService] Initialize method failed:",
error,
);
this.initializationPromise = null; // Reset on failure
throw error;
}
}
private async _initialize(): Promise<void> {
if (this.initialized) {
return;
}
try {
// Create/Open database
this.db = await this.sqlite.createConnection(
this.dbName,
false,
"no-encryption",
1,
false,
);
await this.db.open();
// Set journal mode to WAL for better performance
// await this.db.execute("PRAGMA journal_mode=WAL;");
// Run migrations
await this.runCapacitorMigrations();
this.initialized = true;
logger.log(
"[CapacitorPlatformService] SQLite database initialized successfully",
);
// Start processing the queue after initialization
this.processQueue();
} catch (error) {
logger.error(
"[CapacitorPlatformService] Error initializing SQLite database:",
error,
);
throw new Error(
"[CapacitorPlatformService] Failed to initialize database",
);
}
}
private async processQueue(): Promise<void> {
if (this.isProcessingQueue || !this.initialized || !this.db) {
return;
}
this.isProcessingQueue = true;
while (this.operationQueue.length > 0) {
const operation = this.operationQueue.shift();
if (!operation) continue;
try {
let result: unknown;
switch (operation.type) {
case "run": {
const runResult = await this.db.run(
operation.sql,
operation.params,
);
result = {
changes: runResult.changes?.changes || 0,
lastId: runResult.changes?.lastId,
};
break;
}
case "query": {
const queryResult = await this.db.query(
operation.sql,
operation.params,
);
result = {
columns: Object.keys(queryResult.values?.[0] || {}),
values: (queryResult.values || []).map((row) =>
Object.values(row),
),
};
break;
}
}
operation.resolve(result);
} catch (error) {
logger.error(
"[CapacitorPlatformService] Error while processing SQL queue:",
error,
);
operation.reject(error);
}
}
this.isProcessingQueue = false;
}
private async queueOperation<R>(
type: QueuedOperation["type"],
sql: string,
params: unknown[] = [],
): Promise<R> {
return new Promise<R>((resolve, reject) => {
const operation: QueuedOperation = {
type,
sql,
params,
resolve: (value: unknown) => resolve(value as R),
reject,
};
this.operationQueue.push(operation);
// If we're already initialized, start processing the queue
if (this.initialized && this.db) {
this.processQueue();
}
});
}
private async waitForInitialization(): Promise<void> {
// If we have an initialization promise, wait for it
if (this.initializationPromise) {
await this.initializationPromise;
return;
}
// If not initialized and no promise, start initialization
if (!this.initialized) {
await this.initializeDatabase();
return;
}
// If initialized but no db, something went wrong
if (!this.db) {
logger.error(
"[CapacitorPlatformService] Database not properly initialized after await waitForInitialization() - initialized flag is true but db is null",
);
throw new Error(
"[CapacitorPlatformService] The database could not be initialized. We recommend you restart or reinstall.",
);
}
}
private async runCapacitorMigrations(): Promise<void> {
if (!this.db) {
throw new Error("Database not initialized");
}
const sqlExec: (sql: string) => Promise<capSQLiteChanges> =
this.db.execute.bind(this.db);
const sqlQuery: (sql: string) => Promise<DBSQLiteValues> =
this.db.query.bind(this.db);
const extractMigrationNames: (result: DBSQLiteValues) => Set<string> = (
result,
) => {
const names =
result.values?.map((row: { name: string }) => row.name) || [];
return new Set(names);
};
runMigrations(sqlExec, sqlQuery, extractMigrationNames);
}
/**
* Gets the capabilities of the Capacitor platform
@@ -189,6 +403,9 @@ export class CapacitorPlatformService implements PlatformService {
*/
async writeFile(fileName: string, content: string): Promise<void> {
try {
// Check storage permissions before proceeding
await this.checkStoragePermissions();
const logData = {
targetFileName: fileName,
contentLength: content.length,
@@ -330,6 +547,9 @@ export class CapacitorPlatformService implements PlatformService {
logger.log("[CapacitorPlatformService]", JSON.stringify(logData, null, 2));
try {
// Check storage permissions before proceeding
await this.checkStoragePermissions();
const { uri } = await Filesystem.writeFile({
path: fileName,
data: content,
@@ -490,4 +710,27 @@ export class CapacitorPlatformService implements PlatformService {
// This is just a placeholder for the interface
return Promise.resolve();
}
/**
* @see PlatformService.dbQuery
*/
async dbQuery(sql: string, params?: unknown[]): Promise<QueryExecResult> {
await this.waitForInitialization();
return this.queueOperation<QueryExecResult>("query", sql, params || []);
}
/**
* @see PlatformService.dbExec
*/
async dbExec(
sql: string,
params?: unknown[],
): Promise<{ changes: number; lastId?: number }> {
await this.waitForInitialization();
return this.queueOperation<{ changes: number; lastId?: number }>(
"run",
sql,
params || [],
);
}
}

View File

@@ -4,20 +4,195 @@ import {
PlatformCapabilities,
} from "../PlatformService";
import { logger } from "../../utils/logger";
import { QueryExecResult, SqlValue } from "@/interfaces/database";
import {
SQLiteConnection,
SQLiteDBConnection,
CapacitorSQLite,
Changes,
} from "@capacitor-community/sqlite";
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
interface Migration {
name: string;
sql: string;
}
/**
* Platform service implementation for Electron (desktop) platform.
* Note: This is a placeholder implementation with most methods currently unimplemented.
* Implements the PlatformService interface but throws "Not implemented" errors for most operations.
*
* @remarks
* This service is intended for desktop application functionality through Electron.
* Future implementations should provide:
* - Native file system access
* - Desktop camera integration
* - System-level features
* Provides native desktop functionality through Electron and Capacitor plugins for:
* - File system operations (TODO)
* - Camera integration (TODO)
* - SQLite database operations
* - System-level features (TODO)
*/
export class ElectronPlatformService implements PlatformService {
private sqlite: SQLiteConnection;
private db: SQLiteDBConnection | null = null;
private dbName = "timesafari.db";
private initialized = false;
constructor() {
this.sqlite = new SQLiteConnection(CapacitorSQLite);
}
private async initializeDatabase(): Promise<void> {
if (this.initialized) {
return;
}
try {
// Create/Open database
this.db = await this.sqlite.createConnection(
this.dbName,
false,
"no-encryption",
1,
false,
);
await this.db.open();
// Set journal mode to WAL for better performance
await this.db.execute("PRAGMA journal_mode=WAL;");
// Run migrations
await this.runMigrations();
this.initialized = true;
logger.log(
"[ElectronPlatformService] SQLite database initialized successfully",
);
} catch (error) {
logger.error(
"[ElectronPlatformService] Error initializing SQLite database:",
error,
);
throw new Error(
"[ElectronPlatformService] Failed to initialize database",
);
}
}
private async runMigrations(): Promise<void> {
if (!this.db) {
throw new Error("Database not initialized");
}
// Create migrations table if it doesn't exist
await this.db.execute(`
CREATE TABLE IF NOT EXISTS migrations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
`);
// Get list of executed migrations
const result = await this.db.query("SELECT name FROM migrations;");
const executedMigrations = new Set(
result.values?.map((row) => row[0]) || [],
);
// Run pending migrations in order
const migrations: Migration[] = [
{
name: "001_initial",
sql: `
CREATE TABLE IF NOT EXISTS accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
dateCreated TEXT NOT NULL,
derivationPath TEXT,
did TEXT NOT NULL,
identityEncrBase64 TEXT,
mnemonicEncrBase64 TEXT,
passkeyCredIdHex TEXT,
publicKeyHex TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_accounts_did ON accounts(did);
CREATE TABLE IF NOT EXISTS secret (
id INTEGER PRIMARY KEY AUTOINCREMENT,
secretBase64 TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS settings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
accountDid TEXT,
activeDid TEXT,
apiServer TEXT,
filterFeedByNearby BOOLEAN,
filterFeedByVisible BOOLEAN,
finishedOnboarding BOOLEAN,
firstName TEXT,
hideRegisterPromptOnNewContact BOOLEAN,
isRegistered BOOLEAN,
lastName TEXT,
lastAckedOfferToUserJwtId TEXT,
lastAckedOfferToUserProjectsJwtId TEXT,
lastNotifiedClaimId TEXT,
lastViewedClaimId TEXT,
notifyingNewActivityTime TEXT,
notifyingReminderMessage TEXT,
notifyingReminderTime TEXT,
partnerApiServer TEXT,
passkeyExpirationMinutes INTEGER,
profileImageUrl TEXT,
searchBoxes TEXT,
showContactGivesInline BOOLEAN,
showGeneralAdvanced BOOLEAN,
showShortcutBvc BOOLEAN,
vapid TEXT,
warnIfProdServer BOOLEAN,
warnIfTestServer BOOLEAN,
webPushServer TEXT
);
CREATE INDEX IF NOT EXISTS idx_settings_accountDid ON settings(accountDid);
INSERT INTO settings (id, apiServer) VALUES (1, '${DEFAULT_ENDORSER_API_SERVER}');
CREATE TABLE IF NOT EXISTS contacts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
did TEXT NOT NULL,
name TEXT,
contactMethods TEXT,
nextPubKeyHashB64 TEXT,
notes TEXT,
profileImageUrl TEXT,
publicKeyBase64 TEXT,
seesMe BOOLEAN,
registered BOOLEAN
);
CREATE INDEX IF NOT EXISTS idx_contacts_did ON contacts(did);
CREATE INDEX IF NOT EXISTS idx_contacts_name ON contacts(name);
CREATE TABLE IF NOT EXISTS logs (
date TEXT PRIMARY KEY,
message TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS temp (
id TEXT PRIMARY KEY,
blobB64 TEXT
);
`,
},
];
for (const migration of migrations) {
if (!executedMigrations.has(migration.name)) {
await this.db.execute(migration.sql);
await this.db.run("INSERT INTO migrations (name) VALUES (?)", [
migration.name,
]);
logger.log(`Migration ${migration.name} executed successfully`);
}
}
}
/**
* Gets the capabilities of the Electron platform
* @returns Platform capabilities object
@@ -55,6 +230,17 @@ export class ElectronPlatformService implements PlatformService {
throw new Error("Not implemented");
}
/**
* Writes content to a file and opens the system share dialog.
* @param _fileName - Name of the file to create
* @param _content - Content to write to the file
* @throws Error with "Not implemented" message
* @todo Implement using Electron's dialog and file system APIs
*/
async writeAndShareFile(_fileName: string, _content: string): Promise<void> {
throw new Error("Not implemented");
}
/**
* Deletes a file from the filesystem.
* @param _path - Path to the file to delete
@@ -108,4 +294,55 @@ export class ElectronPlatformService implements PlatformService {
logger.error("handleDeepLink not implemented in Electron platform");
throw new Error("Not implemented");
}
/**
* @see PlatformService.dbQuery
*/
async dbQuery(sql: string, params?: unknown[]): Promise<QueryExecResult> {
await this.initializeDatabase();
if (!this.db) {
throw new Error("Database not initialized");
}
try {
const result = await this.db.query(sql, params || []);
const values = result.values || [];
return {
columns: [], // SQLite plugin doesn't provide column names in query result
values: values as SqlValue[][],
};
} catch (error) {
logger.error("Error executing query:", error);
throw new Error(
`Database query failed: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
/**
* @see PlatformService.dbExec
*/
async dbExec(
sql: string,
params?: unknown[],
): Promise<{ changes: number; lastId?: number }> {
await this.initializeDatabase();
if (!this.db) {
throw new Error("Database not initialized");
}
try {
const result = await this.db.run(sql, params || []);
const changes = result.changes as Changes;
return {
changes: changes?.changes || 0,
lastId: changes?.lastId,
};
} catch (error) {
logger.error("Error executing statement:", error);
throw new Error(
`Database execution failed: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}

View File

@@ -4,6 +4,7 @@ import {
PlatformCapabilities,
} from "../PlatformService";
import { logger } from "../../utils/logger";
import { QueryExecResult } from "@/interfaces/database";
/**
* Platform service implementation for PyWebView platform.
@@ -109,4 +110,26 @@ export class PyWebViewPlatformService implements PlatformService {
logger.error("handleDeepLink not implemented in PyWebView platform");
throw new Error("Not implemented");
}
dbQuery(sql: string, params?: unknown[]): Promise<QueryExecResult> {
throw new Error("Not implemented for " + sql + " with params " + params);
}
dbExec(
sql: string,
params?: unknown[],
): Promise<{ changes: number; lastId?: number }> {
throw new Error("Not implemented for " + sql + " with params " + params);
}
/**
* Should write and share a file using the Python backend.
* @param _fileName - Name of the file to write and share
* @param _content - Content to write to the file
* @throws Error with "Not implemented" message
* @todo Implement file writing and sharing through pywebview's Python-JavaScript bridge
*/
async writeAndShareFile(_fileName: string, _content: string): Promise<void> {
logger.error("writeAndShareFile not implemented in PyWebView platform");
throw new Error("Not implemented");
}
}

View File

@@ -4,6 +4,8 @@ import {
PlatformCapabilities,
} from "../PlatformService";
import { logger } from "../../utils/logger";
import { QueryExecResult } from "@/interfaces/database";
import databaseService from "../AbsurdSqlDatabaseService";
/**
* Platform service implementation for web browser platform.
@@ -359,4 +361,33 @@ export class WebPlatformService implements PlatformService {
async writeAndShareFile(_fileName: string, _content: string): Promise<void> {
throw new Error("File system access not available in web platform");
}
/**
* @see PlatformService.dbQuery
*/
dbQuery(
sql: string,
params?: unknown[],
): Promise<QueryExecResult | undefined> {
return databaseService.query(sql, params).then((result) => result[0]);
}
/**
* @see PlatformService.dbExec
*/
dbExec(
sql: string,
params?: unknown[],
): Promise<{ changes: number; lastId?: number }> {
return databaseService.run(sql, params);
}
async dbGetOneRow(
sql: string,
params?: unknown[],
): Promise<unknown[] | undefined> {
return databaseService
.query(sql, params)
.then((result: QueryExecResult[]) => result[0]?.values[0]);
}
}

View File

@@ -1,6 +1,7 @@
import axios from "axios";
import * as didJwt from "did-jwt";
import { AppString } from "../constants/app";
import { AppString, USE_DEXIE_DB } from "../constants/app";
import * as databaseUtil from "../db/databaseUtil";
import { retrieveSettingsForActiveAccount } from "../db";
import { SERVICE_ID } from "../libs/endorserServer";
import { deriveAddress, newIdentifier } from "../libs/crypto";
@@ -16,7 +17,10 @@ export async function testServerRegisterUser() {
const identity0 = newIdentifier(addr, publicHex, privateHex, deriPath);
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
// Make a claim
const vcClaim = {

45
src/types/absurd-sql.d.ts vendored Normal file
View File

@@ -0,0 +1,45 @@
declare module 'absurd-sql/dist/indexeddb-backend' {
export default class IndexedDBBackend {
constructor(options?: {
dbName?: string;
storeName?: string;
onReady?: () => void;
onError?: (error: Error) => void;
});
init(): Promise<void>;
exec(sql: string, params?: any[]): Promise<any>;
close(): Promise<void>;
}
}
declare module 'absurd-sql/dist/indexeddb-main-thread' {
export function initBackend(worker: Worker): Promise<void>;
export default class IndexedDBMainThread {
constructor(options?: {
dbName?: string;
storeName?: string;
onReady?: () => void;
onError?: (error: Error) => void;
});
init(): Promise<void>;
exec(sql: string, params?: any[]): Promise<any>;
close(): Promise<void>;
}
}
declare module 'absurd-sql' {
export class SQLiteFS {
constructor(fs: unknown, backend: IndexedDBBackend);
init(): Promise<void>;
close(): Promise<void>;
exec(sql: string, params?: any[]): Promise<any>;
prepare(sql: string): Promise<any>;
run(sql: string, params?: any[]): Promise<any>;
get(sql: string, params?: any[]): Promise<any>;
all(sql: string, params?: any[]): Promise<any[]>;
}
export * from 'absurd-sql/dist/indexeddb-backend';
export * from 'absurd-sql/dist/indexeddb-main-thread';
}

36
src/types/global.d.ts vendored Normal file
View File

@@ -0,0 +1,36 @@
import type { QueryExecResult, SqlValue } from "./database";
declare module '@jlongster/sql.js' {
interface SQL {
Database: new (path: string, options?: { filename: boolean }) => Database;
FS: {
mkdir: (path: string) => void;
mount: (fs: any, options: any, path: string) => void;
open: (path: string, flags: string) => any;
close: (stream: any) => void;
};
register_for_idb: (fs: any) => void;
}
interface Database {
exec: (sql: string, params?: unknown[]) => Promise<QueryExecResult[]>;
run: (sql: string, params?: unknown[]) => Promise<{ changes: number; lastId?: number }>;
get: (sql: string, params?: unknown[]) => Promise<SqlValue[]>;
all: (sql: string, params?: unknown[]) => Promise<SqlValue[][]>;
prepare: (sql: string) => Promise<Statement>;
close: () => void;
}
interface Statement {
run: (params?: unknown[]) => Promise<{ changes: number; lastId?: number }>;
get: (params?: unknown[]) => Promise<SqlValue[]>;
all: (params?: unknown[]) => Promise<SqlValue[][]>;
finalize: () => void;
}
const initSqlJs: (options?: {
locateFile?: (file: string) => string;
}) => Promise<SQL>;
export default initSqlJs;
}

67
src/types/modules.d.ts vendored Normal file
View File

@@ -0,0 +1,67 @@
import type { QueryExecResult, SqlValue } from "./database";
declare module '@jlongster/sql.js' {
interface SQL {
Database: new (path: string, options?: { filename: boolean }) => Database;
FS: {
mkdir: (path: string) => void;
mount: (fs: any, options: any, path: string) => void;
open: (path: string, flags: string) => any;
close: (stream: any) => void;
};
register_for_idb: (fs: any) => void;
}
interface Database {
exec: (sql: string, params?: unknown[]) => Promise<QueryExecResult[]>;
run: (sql: string, params?: unknown[]) => Promise<{ changes: number; lastId?: number }>;
get: (sql: string, params?: unknown[]) => Promise<SqlValue[]>;
all: (sql: string, params?: unknown[]) => Promise<SqlValue[][]>;
prepare: (sql: string) => Promise<Statement>;
close: () => void;
}
interface Statement {
run: (params?: unknown[]) => Promise<{ changes: number; lastId?: number }>;
get: (params?: unknown[]) => Promise<SqlValue[]>;
all: (params?: unknown[]) => Promise<SqlValue[][]>;
finalize: () => void;
}
const initSqlJs: (options?: {
locateFile?: (file: string) => string;
}) => Promise<SQL>;
export default initSqlJs;
}
declare module 'absurd-sql' {
import type { SQL } from '@jlongster/sql.js';
export class SQLiteFS {
constructor(fs: any, backend: any);
}
}
declare module 'absurd-sql/dist/indexeddb-backend' {
export default class IndexedDBBackend {
constructor();
}
}
declare module 'absurd-sql/dist/indexeddb-main-thread' {
import type { QueryExecResult } from './database';
export interface SQLiteOptions {
filename?: string;
autoLoad?: boolean;
debug?: boolean;
}
export interface SQLiteDatabase {
exec: (sql: string, params?: unknown[]) => Promise<QueryExecResult[]>;
close: () => Promise<void>;
}
export function initSqlJs(options?: any): Promise<any>;
export function createDatabase(options?: SQLiteOptions): Promise<SQLiteDatabase>;
export function openDatabase(options?: SQLiteOptions): Promise<SQLiteDatabase>;
}

57
src/types/sql.js.d.ts vendored Normal file
View File

@@ -0,0 +1,57 @@
/**
* Type definitions for @jlongster/sql.js
* @author Matthew Raymer
* @description TypeScript declaration file for the SQL.js WASM module with filesystem support
*/
declare module '@jlongster/sql.js' {
export interface FileSystem {
mkdir(path: string): void;
mount(fs: any, opts: any, mountpoint: string): void;
open(path: string, flags: string): FileStream;
close(stream: FileStream): void;
}
export interface FileStream {
node: {
contents: {
readIfFallback(): Promise<void>;
};
};
}
export interface Database {
exec(sql: string, params?: any[]): Promise<QueryExecResult[]>;
prepare(sql: string): Statement;
run(sql: string, params?: any[]): Promise<{ changes: number; lastId?: number }>;
close(): void;
}
export interface QueryExecResult {
columns: string[];
values: any[][];
}
export interface Statement {
bind(params: any[]): void;
step(): boolean;
get(): any[];
getColumnNames(): string[];
reset(): void;
free(): void;
}
export interface InitSqlJsStatic {
(config?: {
locateFile?: (file: string) => string;
wasmBinary?: ArrayBuffer;
}): Promise<{
Database: new (path?: string | Uint8Array, opts?: { filename?: boolean }) => Database;
FS: FileSystem;
register_for_idb: (fs: any) => void;
}>;
}
const initSqlJs: InitSqlJsStatic;
export default initSqlJs;
}

View File

@@ -0,0 +1,2 @@
// Empty module to satisfy Node.js built-in module imports
export default {};

View File

@@ -1,4 +1,4 @@
import { logToDb } from "../db";
import { logToDb } from "../db/databaseUtil";
function safeStringify(obj: unknown) {
const seen = new WeakSet();
@@ -24,8 +24,8 @@ export const logger = {
if (process.env.NODE_ENV !== "production") {
// eslint-disable-next-line no-console
console.debug(message, ...args);
const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
logToDb(message + argsString);
// const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
// logToDb(message + argsString);
}
},
log: (message: string, ...args: unknown[]) => {
@@ -42,7 +42,8 @@ export const logger = {
info: (message: string, ...args: unknown[]) => {
if (
process.env.NODE_ENV !== "production" ||
process.env.VITE_PLATFORM === "capacitor"
process.env.VITE_PLATFORM === "capacitor" ||
process.env.VITE_PLATFORM === "electron"
) {
// eslint-disable-next-line no-console
console.info(message, ...args);
@@ -53,7 +54,8 @@ export const logger = {
warn: (message: string, ...args: unknown[]) => {
if (
process.env.NODE_ENV !== "production" ||
process.env.VITE_PLATFORM === "capacitor"
process.env.VITE_PLATFORM === "capacitor" ||
process.env.VITE_PLATFORM === "electron"
) {
// eslint-disable-next-line no-console
console.warn(message, ...args);

View File

@@ -0,0 +1,17 @@
// Minimal crypto module implementation for browser using Web Crypto API
const crypto = {
...window.crypto,
// Add any Node.js crypto methods that might be needed
randomBytes: (size) => {
const buffer = new Uint8Array(size);
window.crypto.getRandomValues(buffer);
return buffer;
},
createHash: () => ({
update: () => ({
digest: () => new Uint8Array(32), // Return empty hash
}),
}),
};
export default crypto;

View File

@@ -0,0 +1,18 @@
// Minimal fs module implementation for browser
const fs = {
readFileSync: () => {
throw new Error("fs.readFileSync is not supported in browser");
},
writeFileSync: () => {
throw new Error("fs.writeFileSync is not supported in browser");
},
existsSync: () => false,
mkdirSync: () => {},
readdirSync: () => [],
statSync: () => ({
isDirectory: () => false,
isFile: () => false,
}),
};
export default fs;

View File

@@ -0,0 +1,13 @@
// Minimal path module implementation for browser
const path = {
resolve: (...parts) => parts.join("/"),
join: (...parts) => parts.join("/"),
dirname: (p) => p.split("/").slice(0, -1).join("/"),
basename: (p) => p.split("/").pop(),
extname: (p) => {
const parts = p.split(".");
return parts.length > 1 ? "." + parts.pop() : "";
},
};
export default path;

View File

@@ -76,7 +76,8 @@
Set Your Name
</button>
<p class="text-xs text-slate-500 mt-1">
(Don't worry: this is not visible to anyone until you share it with them. It's not sent to any servers.)
(Don't worry: this is not visible to anyone until you share it with
them. It's not sent to any servers.)
</p>
<UserNameDialog ref="userNameDialog" />
</span>
@@ -110,7 +111,10 @@
<font-awesome icon="camera" class="fa-fw" />
</div>
</template>
<!-- If not registered, they don't need to see this at all. We show a prompt to register below. -->
<!--
If not registered, they don't need to see this at all. We show a prompt
to register below.
-->
</div>
<ImageMethodDialog
ref="imageMethodDialog"
@@ -427,12 +431,16 @@
<div class="mb-4 text-center">
{{ limitsMessage }}
</div>
<div>
<div v-if="endorserLimits">
<p class="text-sm">
You have done
<b>{{ endorserLimits?.doneClaimsThisWeek || "?" }} claims</b> out of
<b>{{ endorserLimits?.maxClaimsPerWeek || "?" }}</b> for this week.
Your claims counter resets at
<b
>{{ endorserLimits?.doneClaimsThisWeek || "?" }} claim{{
endorserLimits?.doneClaimsThisWeek === 1 ? "" : "s"
}}</b
>
out of <b>{{ endorserLimits?.maxClaimsPerWeek || "?" }}</b> for this
week. Your claims counter resets at
<b class="whitespace-nowrap">{{
readableDate(endorserLimits?.nextWeekBeginDateTime)
}}</b>
@@ -443,7 +451,9 @@
>{{
endorserLimits?.doneRegistrationsThisMonth || "?"
}}
registrations</b
registration{{
endorserLimits?.doneRegistrationsThisMonth === 1 ? "" : "s"
}}</b
>
out of
<b>{{ endorserLimits?.maxRegistrationsPerMonth || "?" }}</b> for this
@@ -456,9 +466,13 @@
</p>
<p class="mt-3 text-sm">
You have uploaded
<b>{{ imageLimits?.doneImagesThisWeek || "?" }} images</b> out of
<b>{{ imageLimits?.maxImagesPerWeek || "?" }}</b> for this week. Your
image counter resets at
<b
>{{ imageLimits?.doneImagesThisWeek || "?" }} image{{
imageLimits?.doneImagesThisWeek === 1 ? "" : "s"
}}</b
>
out of <b>{{ imageLimits?.maxImagesPerWeek || "?" }}</b> for this
week. Your image counter resets at
<b class="whitespace-nowrap">{{
readableDate(imageLimits?.nextWeekBeginDateTime)
}}</b>
@@ -495,7 +509,7 @@
<!-- Deep Identity Details -->
<span class="text-slate-500 text-sm font-bold mb-2">
Deep Identifier Details
Identifier Details
</span>
<div
id="sectionDeepIdentifier"
@@ -578,20 +592,9 @@
Switch Identifier
</router-link>
<div class="flex mt-4">
<button>
<router-link
:to="{ name: 'statistics' }"
class="block w-fit text-center 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-4 py-2 rounded-md mb-2"
>
See Global Animated History of Giving
</router-link>
</button>
</div>
<div id="sectionImportContactsSettings" class="mt-4">
<h2 class="text-slate-500 text-sm font-bold">
Import Contacts & Settings Database
Import Contacts
</h2>
<div class="ml-4 mt-2">
@@ -622,9 +625,7 @@
class="block text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6"
@click="checkContactImports()"
>
Import Only Contacts
<br />
after comparing
Import Contacts
</button>
</div>
</div>
@@ -902,9 +903,9 @@
</div>
</label>
<div id="sectionPasskeyExpiration" class="flex justify-between">
<div id="sectionPasskeyExpiration" class="flex mt-4 justify-between">
<span>
<span class="text-slate-500 text-sm font-bold mb-2">
<span class="text-slate-500 text-sm font-bold">
Passkey Expiration Minutes
</span>
<br />
@@ -950,10 +951,27 @@
<router-link
:to="{ name: 'logs' }"
class="block w-fit text-center 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-4 py-2 rounded-md mb-2"
class="block w-fit text-center 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-4 py-2 rounded-md mt-2"
>
View Logs
Logs
</router-link>
<router-link
:to="{ name: 'test' }"
class="block w-fit text-center 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-4 py-2 rounded-md mt-2"
>
Test Page
</router-link>
<div class="flex mt-2">
<button>
<router-link
:to="{ name: 'statistics' }"
class="block w-fit text-center 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-4 py-2 rounded-md"
>
See Global Animated History of Giving
</router-link>
</button>
</div>
</section>
</main>
</template>
@@ -965,7 +983,7 @@ import { AxiosError } from "axios";
import { Buffer } from "buffer/";
import Dexie from "dexie";
import "dexie-export-import";
// @ts-ignore - they aren't exporting it but it's there
// @ts-expect-error - they aren't exporting it but it's there
import { ImportProgress } from "dexie-export-import";
import { LeafletMouseEvent } from "leaflet";
import * as R from "ramda";
@@ -990,6 +1008,7 @@ import {
DEFAULT_PUSH_SERVER,
IMAGE_TYPE_PROFILE,
NotificationIface,
USE_DEXIE_DB,
} from "../constants/app";
import {
db,
@@ -1003,6 +1022,7 @@ import {
DEFAULT_PASSKEY_EXPIRATION_MINUTES,
MASTER_SETTINGS_KEY,
} from "../db/tables/settings";
import * as databaseUtil from "../db/databaseUtil";
import {
clearPasskeyToken,
EndorserRateLimits,
@@ -1021,6 +1041,7 @@ import {
} from "../libs/util";
import { UserProfile } from "@/libs/partnerServer";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
const inputImportFileNameRef = ref<Blob>();
@@ -1136,9 +1157,14 @@ export default class AccountViewView extends Vue {
if (error.status === 404) {
// this is ok: the profile is not yet created
} else {
logConsoleAndDb(
databaseUtil.logConsoleAndDb(
"Error loading profile: " + errorStringForLog(error),
);
if (USE_DEXIE_DB) {
logConsoleAndDb(
"Error loading profile: " + errorStringForLog(error),
);
}
this.$notify(
{
group: "alert",
@@ -1187,7 +1213,6 @@ export default class AccountViewView extends Vue {
this.turnOffNotifyingFlags();
}
}
// console.log("Got to the end of 'mounted' call in AccountViewView.");
/**
* Beware! I've seen where we never get to this point because "ready" never resolves.
*/
@@ -1215,8 +1240,11 @@ export default class AccountViewView extends Vue {
* Initializes component state with values from the database or defaults.
*/
async initializeState() {
await db.open();
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
await db.open();
settings = await retrieveSettingsForActiveAccount();
}
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
@@ -1259,42 +1287,67 @@ export default class AccountViewView extends Vue {
async toggleShowContactAmounts() {
this.showContactGives = !this.showContactGives;
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
await databaseUtil.updateDefaultSettings({
showContactGivesInline: this.showContactGives,
});
if (USE_DEXIE_DB) {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
showContactGivesInline: this.showContactGives,
});
}
}
async toggleShowGeneralAdvanced() {
this.showGeneralAdvanced = !this.showGeneralAdvanced;
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
await databaseUtil.updateDefaultSettings({
showGeneralAdvanced: this.showGeneralAdvanced,
});
if (USE_DEXIE_DB) {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
showGeneralAdvanced: this.showGeneralAdvanced,
});
}
}
async toggleProdWarning() {
this.warnIfProdServer = !this.warnIfProdServer;
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
await databaseUtil.updateDefaultSettings({
warnIfProdServer: this.warnIfProdServer,
});
if (USE_DEXIE_DB) {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
warnIfProdServer: this.warnIfProdServer,
});
}
}
async toggleTestWarning() {
this.warnIfTestServer = !this.warnIfTestServer;
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
await databaseUtil.updateDefaultSettings({
warnIfTestServer: this.warnIfTestServer,
});
if (USE_DEXIE_DB) {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
warnIfTestServer: this.warnIfTestServer,
});
}
}
async toggleShowShortcutBvc() {
this.showShortcutBvc = !this.showShortcutBvc;
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
await databaseUtil.updateDefaultSettings({
showShortcutBvc: this.showShortcutBvc,
});
if (USE_DEXIE_DB) {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
showShortcutBvc: this.showShortcutBvc,
});
}
}
readableDate(timeStr: string) {
@@ -1305,9 +1358,18 @@ export default class AccountViewView extends Vue {
* Processes the identity and updates the component's state.
*/
async processIdentity() {
const account: Account | undefined = await retrieveAccountMetadata(
this.activeDid,
let account: Account | undefined = undefined;
const platformService = PlatformServiceFactory.getInstance();
const dbAccount = await platformService.dbQuery(
"SELECT * FROM accounts WHERE did = ?",
[this.activeDid],
);
if (dbAccount) {
account = databaseUtil.mapQueryResultToValues(dbAccount)[0] as Account;
}
if (USE_DEXIE_DB) {
account = await retrieveAccountMetadata(this.activeDid);
}
if (account?.identity) {
const identity = JSON.parse(account.identity as string) as IIdentifier;
this.publicHex = identity.keys[0].publicKeyHex;
@@ -1350,9 +1412,14 @@ export default class AccountViewView extends Vue {
this.$refs.pushNotificationPermission as PushNotificationPermission
).open(DAILY_CHECK_TITLE, async (success: boolean, timeText: string) => {
if (success) {
await db.settings.update(MASTER_SETTINGS_KEY, {
await databaseUtil.updateDefaultSettings({
notifyingNewActivityTime: timeText,
});
if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, {
notifyingNewActivityTime: timeText,
});
}
this.notifyingNewActivity = true;
this.notifyingNewActivityTime = timeText;
}
@@ -1366,9 +1433,14 @@ export default class AccountViewView extends Vue {
text: "", // unused, only here to satisfy type check
callback: async (success) => {
if (success) {
await db.settings.update(MASTER_SETTINGS_KEY, {
await databaseUtil.updateDefaultSettings({
notifyingNewActivityTime: "",
});
if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, {
notifyingNewActivityTime: "",
});
}
this.notifyingNewActivity = false;
this.notifyingNewActivityTime = "";
}
@@ -1410,10 +1482,16 @@ export default class AccountViewView extends Vue {
DIRECT_PUSH_TITLE,
async (success: boolean, timeText: string, message?: string) => {
if (success) {
await db.settings.update(MASTER_SETTINGS_KEY, {
await databaseUtil.updateDefaultSettings({
notifyingReminderMessage: message,
notifyingReminderTime: timeText,
});
if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, {
notifyingReminderMessage: message,
notifyingReminderTime: timeText,
});
}
this.notifyingReminder = true;
this.notifyingReminderMessage = message || "";
this.notifyingReminderTime = timeText;
@@ -1429,10 +1507,16 @@ export default class AccountViewView extends Vue {
text: "", // unused, only here to satisfy type check
callback: async (success) => {
if (success) {
await db.settings.update(MASTER_SETTINGS_KEY, {
await databaseUtil.updateDefaultSettings({
notifyingReminderMessage: "",
notifyingReminderTime: "",
});
if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, {
notifyingReminderMessage: "",
notifyingReminderTime: "",
});
}
this.notifyingReminder = false;
this.notifyingReminderMessage = "";
this.notifyingReminderTime = "";
@@ -1446,30 +1530,47 @@ export default class AccountViewView extends Vue {
public async toggleHideRegisterPromptOnNewContact() {
const newSetting = !this.hideRegisterPromptOnNewContact;
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
await databaseUtil.updateDefaultSettings({
hideRegisterPromptOnNewContact: newSetting,
});
if (USE_DEXIE_DB) {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
hideRegisterPromptOnNewContact: newSetting,
});
}
this.hideRegisterPromptOnNewContact = newSetting;
}
public async updatePasskeyExpiration() {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
await databaseUtil.updateDefaultSettings({
passkeyExpirationMinutes: this.passkeyExpirationMinutes,
});
if (USE_DEXIE_DB) {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
passkeyExpirationMinutes: this.passkeyExpirationMinutes,
});
}
clearPasskeyToken();
this.passkeyExpirationDescription = tokenExpiryTimeDescription();
}
public async turnOffNotifyingFlags() {
// should tell the push server as well
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
await databaseUtil.updateDefaultSettings({
notifyingNewActivityTime: "",
notifyingReminderMessage: "",
notifyingReminderTime: "",
});
if (USE_DEXIE_DB) {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
notifyingNewActivityTime: "",
notifyingReminderMessage: "",
notifyingReminderTime: "",
});
}
this.notifyingNewActivity = false;
this.notifyingNewActivityTime = "";
this.notifyingReminder = false;
@@ -1509,7 +1610,10 @@ export default class AccountViewView extends Vue {
* @returns {Promise<Blob>} The generated blob object.
*/
private async generateDatabaseBlob(): Promise<Blob> {
return await db.export({ prettyJson: true });
if (USE_DEXIE_DB) {
return await db.export({ prettyJson: true });
}
throw new Error("Not implemented");
}
/**
@@ -1530,7 +1634,7 @@ export default class AccountViewView extends Vue {
private downloadDatabaseBackup(url: string) {
const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement;
downloadAnchor.href = url;
downloadAnchor.download = `${db.name}-backup.json`;
downloadAnchor.download = `${AppString.APP_NAME_NO_SPACES}-backup.json`;
downloadAnchor.click(); // doesn't work for some browsers, eg. DuckDuckGo
}
@@ -1611,25 +1715,30 @@ export default class AccountViewView extends Vue {
*/
async submitImportFile() {
if (inputImportFileNameRef.value != null) {
await db.delete()
.then(async () => {
// BulkError: settings.bulkAdd(): 1 of 21 operations failed. Errors: ConstraintError: Key already exists in the object store.
await Dexie.import(inputImportFileNameRef.value as Blob, {
progressCallback: this.progressCallback,
if (USE_DEXIE_DB) {
await db
.delete()
.then(async () => {
// BulkError: settings.bulkAdd(): 1 of 21 operations failed. Errors: ConstraintError: Key already exists in the object store.
await Dexie.import(inputImportFileNameRef.value as Blob, {
progressCallback: this.progressCallback,
});
})
})
.catch((error) => {
logger.error("Error importing file:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Importing",
text: "There was an error in the import. Your identities and contacts may have been affected, so you may have to restore your identifier and use the contact import method.",
},
-1,
);
});
.catch((error) => {
logger.error("Error importing file:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Importing",
text: "There was an error in the import. Your identities and contacts may have been affected, so you may have to restore your identifier and use the contact import method.",
},
-1,
);
});
} else {
throw new Error("Not implemented");
}
}
}
@@ -1717,7 +1826,15 @@ 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 updateAccountSettings(did, { isRegistered: true });
await databaseUtil.updateAccountSettings(did, {
isRegistered: true,
});
if (USE_DEXIE_DB) {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
isRegistered: true,
});
}
this.isRegistered = true;
} catch (err) {
logger.error("Got an error updating settings:", err);
@@ -1777,26 +1894,41 @@ export default class AccountViewView extends Vue {
}
async onClickSaveApiServer() {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
await databaseUtil.updateDefaultSettings({
apiServer: this.apiServerInput,
});
if (USE_DEXIE_DB) {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
apiServer: this.apiServerInput,
});
}
this.apiServer = this.apiServerInput;
}
async onClickSavePartnerServer() {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
await databaseUtil.updateDefaultSettings({
partnerApiServer: this.partnerApiServerInput,
});
if (USE_DEXIE_DB) {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
partnerApiServer: this.partnerApiServerInput,
});
}
this.partnerApiServer = this.partnerApiServerInput;
}
async onClickSavePushServer() {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
await databaseUtil.updateDefaultSettings({
webPushServer: this.webPushServerInput,
});
if (USE_DEXIE_DB) {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
webPushServer: this.webPushServerInput,
});
}
this.webPushServer = this.webPushServerInput;
this.$notify(
{
@@ -1812,10 +1944,15 @@ export default class AccountViewView extends Vue {
openImageDialog() {
(this.$refs.imageMethodDialog as ImageMethodDialog).open(
async (imgUrl) => {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
await databaseUtil.updateDefaultSettings({
profileImageUrl: imgUrl,
});
if (USE_DEXIE_DB) {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
profileImageUrl: imgUrl,
});
}
this.profileImageUrl = imgUrl;
//console.log("Got image URL:", imgUrl);
},
@@ -1876,10 +2013,15 @@ export default class AccountViewView extends Vue {
// keep the imageUrl in localStorage so the user can try again if they want
}
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
await databaseUtil.updateDefaultSettings({
profileImageUrl: undefined,
});
if (USE_DEXIE_DB) {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
profileImageUrl: undefined,
});
}
this.profileImageUrl = undefined;
} catch (error) {
@@ -1888,9 +2030,14 @@ export default class AccountViewView extends Vue {
if ((error as any).response.status === 404) {
logger.error("The image was already deleted:", error);
await updateAccountSettings(this.activeDid, {
await databaseUtil.updateAccountSettings(this.activeDid, {
profileImageUrl: undefined,
});
if (USE_DEXIE_DB) {
await updateAccountSettings(this.activeDid, {
profileImageUrl: undefined,
});
}
this.profileImageUrl = undefined;
@@ -1968,7 +2115,12 @@ export default class AccountViewView extends Vue {
throw Error("Profile not saved");
}
} catch (error) {
logConsoleAndDb("Error saving profile: " + errorStringForLog(error));
databaseUtil.logConsoleAndDb(
"Error saving profile: " + errorStringForLog(error),
);
if (USE_DEXIE_DB) {
logConsoleAndDb("Error saving profile: " + errorStringForLog(error));
}
const errorMessage: string =
error.response?.data?.error?.message ||
error.response?.data?.error ||
@@ -2058,7 +2210,12 @@ export default class AccountViewView extends Vue {
throw Error("Profile not deleted");
}
} catch (error) {
logConsoleAndDb("Error deleting profile: " + errorStringForLog(error));
databaseUtil.logConsoleAndDb(
"Error deleting profile: " + errorStringForLog(error),
);
if (USE_DEXIE_DB) {
logConsoleAndDb("Error deleting profile: " + errorStringForLog(error));
}
const errorMessage: string =
error.response?.data?.error?.message ||
error.response?.data?.error ||

View File

@@ -33,8 +33,10 @@ import { Component, Vue } from "vue-facing-decorator";
import { AxiosInstance } from "axios";
import QuickNav from "../components/QuickNav.vue";
import { NotificationIface } from "../constants/app";
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from "../db/index";
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
import * as databaseUtil from "../db/databaseUtil";
import { retrieveSettingsForActiveAccount } from "../db/index";
import { logConsoleAndDb } from "../db/databaseUtil";
import * as serverUtil from "../libs/endorserServer";
import * as libsUtil from "../libs/util";
import { errorStringForLog } from "../libs/endorserServer";
@@ -77,7 +79,10 @@ export default class ClaimAddRawView extends Vue {
* Initialize settings from active account
*/
private async initializeSettings() {
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
}

View File

@@ -14,11 +14,14 @@
import { Component, Vue } from "vue-facing-decorator";
import { nextTick } from "vue";
import QRCode from "qrcode";
import { APP_SERVER, NotificationIface } from "../constants/app";
import { APP_SERVER, NotificationIface, USE_DEXIE_DB } from "../constants/app";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
import * as databaseUtil from "../db/databaseUtil";
import * as serverUtil from "../libs/endorserServer";
import { GenericCredWrapper, GenericVerifiableCredential } from "../interfaces";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { Contact } from "@/db/tables/contacts";
@Component
export default class ClaimCertificateView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
@@ -32,7 +35,10 @@ export default class ClaimCertificateView extends Vue {
serverUtil = serverUtil;
async created() {
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
const pathParams = window.location.pathname.substring(
@@ -84,8 +90,17 @@ export default class ClaimCertificateView extends Vue {
claimData: GenericCredWrapper<GenericVerifiableCredential>,
confirmerIds: Array<string>,
) {
await db.open();
const allContacts = await db.contacts.toArray();
const platformService = PlatformServiceFactory.getInstance();
const dbAllContacts = await platformService.dbQuery(
"SELECT * FROM contacts",
);
let allContacts = databaseUtil.mapQueryResultToValues(
dbAllContacts,
) as unknown as Contact[];
if (USE_DEXIE_DB) {
await db.open();
allContacts = await db.contacts.toArray();
}
const canvas = this.$refs.claimCanvas as HTMLCanvasElement;
if (canvas) {

View File

@@ -11,10 +11,13 @@ import { Component, Vue } from "vue-facing-decorator";
import { nextTick } from "vue";
import QRCode from "qrcode";
import { APP_SERVER, NotificationIface } from "../constants/app";
import { APP_SERVER, NotificationIface, USE_DEXIE_DB } from "../constants/app";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
import * as databaseUtil from "../db/databaseUtil";
import * as endorserServer from "../libs/endorserServer";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { Contact } from "@/db/tables/contacts";
@Component
export default class ClaimReportCertificateView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
@@ -28,7 +31,10 @@ export default class ClaimReportCertificateView extends Vue {
endorserServer = endorserServer;
async created() {
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
const pathParams = window.location.pathname.substring(
@@ -66,8 +72,17 @@ export default class ClaimReportCertificateView extends Vue {
async drawCanvas(
claimData: endorserServer.GenericCredWrapper<endorserServer.GenericVerifiableCredential>,
) {
await db.open();
const allContacts = await db.contacts.toArray();
const platformService = PlatformServiceFactory.getInstance();
const dbAllContacts = await platformService.dbQuery(
"SELECT * FROM contacts",
);
let allContacts = databaseUtil.mapQueryResultToValues(
dbAllContacts,
) as unknown as Contact[];
if (USE_DEXIE_DB) {
await db.open();
allContacts = await db.contacts.toArray();
}
const canvas = this.$refs.claimCanvas as HTMLCanvasElement;
if (canvas) {

View File

@@ -542,12 +542,10 @@ import { useClipboard } from "@vueuse/core";
import { GenericVerifiableCredential } from "../interfaces";
import GiftedDialog from "../components/GiftedDialog.vue";
import QuickNav from "../components/QuickNav.vue";
import { NotificationIface } from "../constants/app";
import {
db,
logConsoleAndDb,
retrieveSettingsForActiveAccount,
} from "../db/index";
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
import * as databaseUtil from "../db/databaseUtil";
import { db } from "../db/index";
import { logConsoleAndDb } from "../db/databaseUtil";
import { Contact } from "../db/tables/contacts";
import * as serverUtil from "../libs/endorserServer";
import {
@@ -557,6 +555,7 @@ import {
} from "../interfaces";
import * as libsUtil from "../libs/util";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
@Component({
components: { GiftedDialog, QuickNav },
@@ -620,11 +619,24 @@ export default class ClaimView extends Vue {
}
async created() {
logger.log("ClaimView created");
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await databaseUtil.retrieveSettingsForActiveAccount();
}
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.allContacts = await db.contacts.toArray();
const platformService = PlatformServiceFactory.getInstance();
const dbAllContacts = await platformService.dbQuery(
"SELECT * FROM contacts",
);
this.allContacts = databaseUtil.mapQueryResultToValues(
dbAllContacts,
) as unknown as Contact[];
if (USE_DEXIE_DB) {
await db.open();
this.allContacts = await db.contacts.toArray();
}
this.isRegistered = settings.isRegistered || false;
try {
@@ -690,7 +702,6 @@ export default class ClaimView extends Vue {
}
async loadClaim(claimId: string, userDid: string) {
logger.log("[ClaimView] loadClaim called with claimId:", claimId);
const urlPath = libsUtil.isGlobalUri(claimId)
? "/api/claim/byHandle/"
: "/api/claim/";
@@ -698,7 +709,6 @@ export default class ClaimView extends Vue {
const headers = await serverUtil.getHeaders(userDid);
try {
logger.log("[ClaimView] Making API request to:", url);
const resp = await this.axios.get(url, { headers });
if (resp.status === 200) {
this.veriClaim = resp.data;

View File

@@ -438,9 +438,10 @@ import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import { NotificationIface } from "../constants/app";
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
import { Contact } from "../db/tables/contacts";
import * as databaseUtil from "../db/databaseUtil";
import * as serverUtil from "../libs/endorserServer";
import { GenericVerifiableCredential, GiveSummaryRecord } from "../interfaces";
import { displayAmount } from "../libs/endorserServer";
@@ -448,6 +449,7 @@ import * as libsUtil from "../libs/util";
import { retrieveAccountDids } from "../libs/util";
import TopMessage from "../components/TopMessage.vue";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
/**
* ConfirmGiftView Component
*
@@ -528,10 +530,23 @@ export default class ConfirmGiftView extends Vue {
* Initializes component settings and user data
*/
private async initializeSettings() {
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.allContacts = await db.contacts.toArray();
const platformService = PlatformServiceFactory.getInstance();
const dbAllContacts = await platformService.dbQuery(
"SELECT * FROM contacts",
);
this.allContacts = databaseUtil.mapQueryResultToValues(
dbAllContacts,
) as unknown as Contact[];
if (USE_DEXIE_DB) {
await db.open();
this.allContacts = await db.contacts.toArray();
}
this.isRegistered = settings.isRegistered || false;
this.allMyDids = await retrieveAccountDids();

View File

@@ -117,9 +117,10 @@ import { Component, Vue } from "vue-facing-decorator";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import { NotificationIface } from "../constants/app";
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
import { Contact } from "../db/tables/contacts";
import * as databaseUtil from "../db/databaseUtil";
import {
AgreeVerifiableCredential,
GiveSummaryRecord,
@@ -133,6 +134,7 @@ import {
} from "../libs/endorserServer";
import { retrieveAccountCount } from "../libs/util";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
@Component({ components: { QuickNav } })
export default class ContactAmountssView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
@@ -154,9 +156,23 @@ export default class ContactAmountssView extends Vue {
async created() {
try {
const contactDid = this.$route.query["contactDid"] as string;
this.contact = (await db.contacts.get(contactDid)) || null;
const platformService = PlatformServiceFactory.getInstance();
const dbContact = await platformService.dbQuery(
"SELECT * FROM contacts WHERE did = ?",
[contactDid],
);
this.contact = databaseUtil.mapQueryResultToValues(
dbContact,
)[0] as unknown as Contact;
if (USE_DEXIE_DB) {
await db.open();
this.contact = (await db.contacts.get(contactDid)) || null;
}
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";

View File

@@ -138,9 +138,11 @@ import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
import { AppString, NotificationIface } from "../constants/app";
import { AppString, NotificationIface, USE_DEXIE_DB } from "../constants/app";
import { db } from "../db/index";
import { Contact, ContactMethod } from "../db/tables/contacts";
import * as databaseUtil from "../db/databaseUtil";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
/**
* Contact Edit View Component
@@ -188,7 +190,7 @@ export default class ContactEditView extends Vue {
$router!: Router;
/** Current contact data */
contact: Contact = {
contact: Contact | undefined = {
did: "",
name: "",
notes: "",
@@ -220,7 +222,21 @@ export default class ContactEditView extends Vue {
*/
async created() {
const contactDid = this.$route.params.did;
const contact = await db.contacts.get(contactDid || "");
const platformService = PlatformServiceFactory.getInstance();
const dbContact = await platformService.dbQuery(
"SELECT * FROM contacts WHERE did = ?",
[contactDid],
);
let contact: Contact | undefined = databaseUtil.mapQueryResultToValues(
dbContact,
)[0] as unknown as Contact;
contact.contactMethods = JSON.parse(
(contact?.contactMethods as unknown as string) || "[]",
);
if (USE_DEXIE_DB) {
await db.open();
contact = await db.contacts.get(contactDid || "");
}
if (contact) {
this.contact = contact;
this.contactName = contact.name || "";
@@ -322,12 +338,24 @@ export default class ContactEditView extends Vue {
}
// Save to database
await db.contacts.update(this.contact.did, {
name: this.contactName,
notes: this.contactNotes,
contactMethods: contactMethods,
});
const platformService = PlatformServiceFactory.getInstance();
const contactMethodsString = JSON.stringify(contactMethods);
await platformService.dbExec(
"UPDATE contacts SET name = ?, notes = ?, contactMethods = ? WHERE did = ?",
[
this.contactName,
this.contactNotes,
contactMethodsString,
this.contact?.did || "",
],
);
if (USE_DEXIE_DB) {
await db.contacts.update(this.contact?.did || "", {
name: this.contactName,
notes: this.contactNotes,
contactMethods: contactMethods,
});
}
// Notify success and redirect
this.$notify({
group: "alert",
@@ -336,7 +364,7 @@ export default class ContactEditView extends Vue {
text: "The contact info has been updated successfully.",
});
(this.$router as Router).push({
path: "/did/" + encodeURIComponent(this.contact.did),
path: "/did/" + encodeURIComponent(this.contact?.did || ""),
});
}
}

View File

@@ -76,11 +76,13 @@ import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import GiftedDialog from "../components/GiftedDialog.vue";
import QuickNav from "../components/QuickNav.vue";
import EntityIcon from "../components/EntityIcon.vue";
import { NotificationIface } from "../constants/app";
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
import { Contact } from "../db/tables/contacts";
import * as databaseUtil from "../db/databaseUtil";
import { GiverReceiverInputInfo } from "../libs/util";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
@Component({
components: { GiftedDialog, QuickNav, EntityIcon },
})
@@ -98,16 +100,29 @@ export default class ContactGiftingView extends Vue {
async created() {
try {
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.apiServer = settings.apiServer || "";
this.activeDid = settings.activeDid || "";
// .orderBy("name") wouldn't retrieve any entries with a blank name
// .toCollection.sortBy("name") didn't sort in an order I understood
const baseContacts = await db.contacts.toArray();
this.allContacts = baseContacts.sort((a, b) =>
(a.name || "").localeCompare(b.name || ""),
const platformService = PlatformServiceFactory.getInstance();
const dbAllContacts = await platformService.dbQuery(
"SELECT * FROM contacts ORDER BY name",
);
this.allContacts = databaseUtil.mapQueryResultToValues(
dbAllContacts,
) as unknown as Contact[];
if (USE_DEXIE_DB) {
await db.open();
// .orderBy("name") wouldn't retrieve any entries with a blank name
// .toCollection.sortBy("name") didn't sort in an order I understood
const baseContacts = await db.contacts.toArray();
this.allContacts = baseContacts.sort((a, b) =>
(a.name || "").localeCompare(b.name || ""),
);
}
this.projectId = (this.$route.query["projectId"] as string) || "";
this.prompt = (this.$route.query["prompt"] as string) ?? this.prompt;

View File

@@ -200,13 +200,19 @@ import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import EntityIcon from "../components/EntityIcon.vue";
import OfferDialog from "../components/OfferDialog.vue";
import { APP_SERVER, AppString, NotificationIface } from "../constants/app";
import {
APP_SERVER,
AppString,
NotificationIface,
USE_DEXIE_DB,
} from "../constants/app";
import {
db,
logConsoleAndDb,
retrieveSettingsForActiveAccount,
} from "../db/index";
import { Contact, ContactMethod } from "../db/tables/contacts";
import * as databaseUtil from "../db/databaseUtil";
import * as libsUtil from "../libs/util";
import {
capitalizeAndInsertSpacesBeforeCaps,
@@ -215,6 +221,77 @@ import {
} from "../libs/endorserServer";
import { getContactJwtFromJwtUrl } from "../libs/crypto";
import { decodeEndorserJwt } from "../libs/crypto/vc";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
/**
* Interface for contact data as stored in the database
* Differs from Contact interface in that contactMethods is stored as a JSON string
*/
interface ContactDbRecord {
did: string;
contactMethods: string;
name: string;
notes: string;
profileImageUrl: string;
publicKeyBase64: string;
nextPubKeyHashB64: string;
seesMe: boolean;
registered: boolean;
}
/**
* Ensures a value is a string, never null or undefined
*/
function safeString(val: unknown): string {
return typeof val === "string" ? val : val == null ? "" : String(val);
}
/**
* Converts a Contact object to a ContactDbRecord for database storage
* @param contact The contact object to convert
* @returns A ContactDbRecord with contactMethods as a JSON string
* @throws Error if contact.did is missing or invalid
*/
function contactToDbRecord(contact: Contact): ContactDbRecord {
if (!contact.did) {
throw new Error("Contact must have a DID");
}
// Convert contactMethods array to JSON string, defaulting to empty array
const contactMethodsStr =
contact.contactMethods != null
? JSON.stringify(contact.contactMethods)
: "[]";
return {
did: safeString(contact.did), // Required field, must be present
contactMethods: contactMethodsStr,
name: safeString(contact.name),
notes: safeString(contact.notes),
profileImageUrl: safeString(contact.profileImageUrl),
publicKeyBase64: safeString(contact.publicKeyBase64),
nextPubKeyHashB64: safeString(contact.nextPubKeyHashB64),
seesMe: contact.seesMe ?? false,
registered: contact.registered ?? false,
};
}
/**
* Converts a ContactDbRecord back to a Contact object
* @param record The database record to convert
* @returns A Contact object with parsed contactMethods array
*/
function dbRecordToContact(record: ContactDbRecord): Contact {
return {
...record,
name: safeString(record.name),
notes: safeString(record.notes),
profileImageUrl: safeString(record.profileImageUrl),
publicKeyBase64: safeString(record.publicKeyBase64),
nextPubKeyHashB64: safeString(record.nextPubKeyHashB64),
contactMethods: JSON.parse(record.contactMethods || "[]"),
};
}
/**
* Contact Import View Component
@@ -337,7 +414,10 @@ export default class ContactImportView extends Vue {
* Initializes component settings from active account
*/
private async initializeSettings() {
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
}
@@ -401,8 +481,17 @@ export default class ContactImportView extends Vue {
this.contactsImporting = contacts;
this.contactsSelected = new Array(this.contactsImporting.length).fill(true);
await db.open();
const baseContacts = await db.contacts.toArray();
const platformService = PlatformServiceFactory.getInstance();
const dbAllContacts = await platformService.dbQuery(
"SELECT * FROM contacts",
);
let baseContacts = databaseUtil.mapQueryResultToValues(
dbAllContacts,
) as unknown as Contact[];
if (USE_DEXIE_DB) {
await db.open();
baseContacts = await db.contacts.toArray();
}
// Check for existing contacts and differences
for (let i = 0; i < this.contactsImporting.length; i++) {
@@ -514,13 +603,39 @@ export default class ContactImportView extends Vue {
if (this.contactsSelected[i]) {
const contact = this.contactsImporting[i];
const existingContact = this.contactsExisting[contact.did];
const platformService = PlatformServiceFactory.getInstance();
// Convert contact to database record format
const contactToStore = contactToDbRecord(contact);
if (existingContact) {
await db.contacts.update(contact.did, contact);
// Update existing contact
const { sql, params } = databaseUtil.generateUpdateStatement(
contactToStore as unknown as Record<string, unknown>,
"contacts",
"did = ?",
[contact.did],
);
await platformService.dbExec(sql, params);
if (USE_DEXIE_DB) {
// For Dexie, we need to parse the contactMethods back to an array
await db.contacts.update(
contact.did,
dbRecordToContact(contactToStore),
);
}
updatedCount++;
} else {
// without explicit clone on the Proxy, we get: DataCloneError: Failed to execute 'add' on 'IDBObjectStore': #<Object> could not be cloned.
// DataError: Failed to execute 'add' on 'IDBObjectStore': Evaluating the object store's key path yielded a value that is not a valid key.
await db.contacts.add(R.clone(contact));
// Add new contact
const { sql, params } = databaseUtil.generateInsertStatement(
contactToStore as unknown as Record<string, unknown>,
"contacts",
);
await platformService.dbExec(sql, params);
if (USE_DEXIE_DB) {
// For Dexie, we need to parse the contactMethods back to an array
await db.contacts.add(dbRecordToContact(contactToStore));
}
importedCount++;
}
}

View File

@@ -104,23 +104,26 @@
</template>
<script lang="ts">
import QRCodeVue3 from "qr-code-generator-vue3";
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { useClipboard } from "@vueuse/core";
import { logger } from "../utils/logger";
import { QRScannerFactory } from "../services/QRScanner/QRScannerFactory";
import QuickNav from "../components/QuickNav.vue";
import { NotificationIface } from "../constants/app";
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
import { db } from "../db/index";
import { Contact } from "../db/tables/contacts";
import { getContactJwtFromJwtUrl } from "../libs/crypto";
import { decodeEndorserJwt, ETHR_DID_PREFIX } from "../libs/crypto/vc";
import { retrieveSettingsForActiveAccount } from "../db/index";
import * as databaseUtil from "../db/databaseUtil";
import { setVisibilityUtil } from "../libs/endorserServer";
import { useClipboard } from "@vueuse/core";
import QRCodeVue3 from "qr-code-generator-vue3";
import UserNameDialog from "../components/UserNameDialog.vue";
import { generateEndorserJwtUrlForAccount } from "../libs/endorserServer";
import { retrieveAccountMetadata } from "../libs/util";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
interface QRScanResult {
rawValue?: string;
@@ -161,7 +164,10 @@ export default class ContactQRScan extends Vue {
async created() {
try {
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.givenName = settings.firstName || "";
@@ -435,13 +441,22 @@ export default class ContactQRScan extends Vue {
async addNewContact(contact: Contact) {
try {
logger.info("Opening database connection for new contact");
await db.open();
// Check if contact already exists
const existingContacts = await db.contacts.toArray();
const existingContact = existingContacts.find(
(c) => c.did === contact.did,
const platformService = PlatformServiceFactory.getInstance();
const dbAllContacts = await platformService.dbQuery(
"SELECT * FROM contacts WHERE did = ?",
[contact.did],
);
const existingContacts = databaseUtil.mapQueryResultToValues(
dbAllContacts,
) as unknown as Contact[];
let existingContact: Contact | undefined = existingContacts[0];
if (USE_DEXIE_DB) {
await db.open();
const existingContacts = await db.contacts.toArray();
existingContact = existingContacts.find((c) => c.did === contact.did);
}
if (existingContact) {
logger.info("Contact already exists", { did: contact.did });
@@ -458,7 +473,16 @@ export default class ContactQRScan extends Vue {
}
// Add new contact
await db.contacts.add(contact);
// @ts-expect-error because we're just using the value to store to the DB
contact.contactMethods = JSON.stringify(contact.contactMethods);
const { sql, params } = databaseUtil.generateInsertStatement(
contact as unknown as Record<string, unknown>,
"contacts",
);
await platformService.dbExec(sql, params);
if (USE_DEXIE_DB) {
await db.contacts.add(contact);
}
if (this.activeDid) {
logger.info("Setting contact visibility", { did: contact.did });

View File

@@ -166,10 +166,11 @@ import { QrcodeStream } from "vue-qrcode-reader";
import QuickNav from "../components/QuickNav.vue";
import UserNameDialog from "../components/UserNameDialog.vue";
import { NotificationIface } from "../constants/app";
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
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 { getContactJwtFromJwtUrl } from "../libs/crypto";
import {
generateEndorserJwtUrlForAccount,
@@ -182,6 +183,7 @@ import { Router } from "vue-router";
import { logger } from "../utils/logger";
import { QRScannerFactory } from "@/services/QRScanner/QRScannerFactory";
import { CameraState } from "@/services/QRScanner/types";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
interface QRScanResult {
rawValue?: string;
@@ -238,7 +240,10 @@ export default class ContactQRScanShow extends Vue {
async created() {
try {
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.givenName = settings.firstName || "";
@@ -569,7 +574,14 @@ export default class ContactQRScanShow extends Vue {
);
if (regResult.success) {
contact.registered = true;
db.contacts.update(contact.did, { registered: true });
const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec(
"UPDATE contacts SET registered = ? WHERE did = ?",
[true, contact.did],
);
if (USE_DEXIE_DB) {
await db.contacts.update(contact.did, { registered: true });
}
logger.info("Contact registration successful", { did: contact.did });
this.$notify(
@@ -701,6 +713,16 @@ export default class ContactQRScanShow extends Vue {
document.addEventListener("resume", this.handleAppResume);
// Start scanning automatically when view is loaded
this.startScanning();
// Apply mirroring after a short delay to ensure video element is ready
setTimeout(() => {
const videoElement = document.querySelector(
".qr-scanner video",
) as HTMLVideoElement;
if (videoElement) {
videoElement.style.transform = "scaleX(-1)";
}
}, 1000);
}
beforeDestroy() {
@@ -727,13 +749,22 @@ export default class ContactQRScanShow extends Vue {
async addNewContact(contact: Contact) {
try {
logger.info("Opening database connection for new contact");
await db.open();
// Check if contact already exists
const existingContacts = await db.contacts.toArray();
const existingContact = existingContacts.find(
(c) => c.did === contact.did,
const platformService = PlatformServiceFactory.getInstance();
const dbAllContacts = await platformService.dbQuery(
"SELECT * FROM contacts WHERE did = ?",
[contact.did],
);
const existingContacts = databaseUtil.mapQueryResultToValues(
dbAllContacts,
) as unknown as Contact[];
let existingContact: Contact | undefined = existingContacts[0];
if (USE_DEXIE_DB) {
await db.open();
const existingContacts = await db.contacts.toArray();
existingContact = existingContacts.find((c) => c.did === contact.did);
}
if (existingContact) {
logger.info("Contact already exists", { did: contact.did });
@@ -750,7 +781,16 @@ export default class ContactQRScanShow extends Vue {
}
// Add new contact
await db.contacts.add(contact);
// @ts-expect-error because we're just using the value to store to the DB
contact.contactMethods = JSON.stringify(contact.contactMethods);
const { sql, params } = databaseUtil.generateInsertStatement(
contact as unknown as Record<string, unknown>,
"contacts",
);
await platformService.dbExec(sql, params);
if (USE_DEXIE_DB) {
await db.contacts.add(contact);
}
if (this.activeDid) {
logger.info("Setting contact visibility", { did: contact.did });
@@ -784,17 +824,31 @@ export default class ContactQRScanShow extends Vue {
text: "Do you want to register them?",
onCancel: async (stopAsking?: boolean) => {
if (stopAsking) {
await db.settings.update(MASTER_SETTINGS_KEY, {
hideRegisterPromptOnNewContact: stopAsking,
});
const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec(
"UPDATE settings SET hideRegisterPromptOnNewContact = ? WHERE id = ?",
[stopAsking, MASTER_SETTINGS_KEY],
);
if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, {
hideRegisterPromptOnNewContact: stopAsking,
});
}
this.hideRegisterPromptOnNewContact = stopAsking;
}
},
onNo: async (stopAsking?: boolean) => {
if (stopAsking) {
await db.settings.update(MASTER_SETTINGS_KEY, {
hideRegisterPromptOnNewContact: stopAsking,
});
const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec(
"UPDATE settings SET hideRegisterPromptOnNewContact = ? WHERE id = ?",
[stopAsking, MASTER_SETTINGS_KEY],
);
if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, {
hideRegisterPromptOnNewContact: stopAsking,
});
}
this.hideRegisterPromptOnNewContact = stopAsking;
}
},

View File

@@ -175,117 +175,119 @@
data-testId="contactListItem"
>
<div class="grow overflow-hidden">
<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 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)
"
/>
<EntityIcon
:contact="contact"
:icon-size="48"
class="inline-block 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) }}
</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>
<span class="text-sm overflow-hidden">{{
libsUtil.shortDid(contact.did)
}}</span>
<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>
<div class="text-sm">
{{ contact.notes }}
</div>
</span>
</div>
<div
v-if="showGiveNumbers && contact.did != activeDid"
class="ml-auto flex gap-1.5 mt-2"
>
<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>
<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>
<h2 class="text-base font-semibold w-1/3 truncate flex-shrink-0">
{{ contactNameNonBreakingSpace(contact.name) }}
</h2>
<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>
<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>
<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>
<span class="text-sm overflow-hidden">{{
libsUtil.shortDid(contact.did)
}}</span>
</div>
<div class="text-sm">
{{ contact.notes }}
</div>
</span>
</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-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>
<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>
</div>
</div>
</li>
@@ -356,7 +358,12 @@ import GiftedDialog from "../components/GiftedDialog.vue";
import OfferDialog from "../components/OfferDialog.vue";
import ContactNameDialog from "../components/ContactNameDialog.vue";
import TopMessage from "../components/TopMessage.vue";
import { APP_SERVER, AppString, NotificationIface } from "../constants/app";
import {
APP_SERVER,
AppString,
NotificationIface,
USE_DEXIE_DB,
} from "../constants/app";
import {
db,
logConsoleAndDb,
@@ -365,6 +372,7 @@ import {
updateDefaultSettings,
} from "../db/index";
import { Contact } from "../db/tables/contacts";
import * as databaseUtil from "../db/databaseUtil";
import { getContactJwtFromJwtUrl } from "../libs/crypto";
import { decodeEndorserJwt } from "../libs/crypto/vc";
import {
@@ -385,6 +393,7 @@ import {
import * as libsUtil from "../libs/util";
import { generateSaveAndActivateIdentity } from "../libs/util";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
@Component({
components: {
GiftedDialog,
@@ -433,8 +442,11 @@ export default class ContactsView extends Vue {
libsUtil = libsUtil;
public async created() {
await db.open();
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
await db.open();
settings = await retrieveSettingsForActiveAccount();
}
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.isRegistered = !!settings.isRegistered;
@@ -452,12 +464,22 @@ export default class ContactsView extends Vue {
this.loadGives();
}
// .orderBy("name") wouldn't retrieve any entries with a blank name
// .toCollection.sortBy("name") didn't sort in an order I understood
const baseContacts = await db.contacts.toArray();
this.contacts = baseContacts.sort((a, b) =>
(a.name || "").localeCompare(b.name || ""),
const platformService = PlatformServiceFactory.getInstance();
const dbAllContacts = await platformService.dbQuery(
"SELECT * FROM contacts ORDER BY name",
);
this.contacts = databaseUtil.mapQueryResultToValues(
dbAllContacts,
) as unknown as Contact[];
if (USE_DEXIE_DB) {
await db.open();
// .orderBy("name") wouldn't retrieve any entries with a blank name
// .toCollection.sortBy("name") didn't sort in an order I understood
const baseContacts = await db.contacts.toArray();
this.contacts = baseContacts.sort((a, b) =>
(a.name || "").localeCompare(b.name || ""),
);
}
}
private async processContactJwt() {
@@ -514,7 +536,12 @@ export default class ContactsView extends Vue {
if (response.status != 201) {
throw { error: { response: response } };
}
await updateAccountSettings(this.activeDid, { isRegistered: true });
await databaseUtil.updateAccountSettings(this.activeDid, {
isRegistered: true,
});
if (USE_DEXIE_DB) {
await updateAccountSettings(this.activeDid, { isRegistered: true });
}
this.isRegistered = true;
this.$notify(
{
@@ -815,12 +842,22 @@ export default class ContactsView extends Vue {
this.danger("An error occurred. Some contacts may have been added.");
}
// .orderBy("name") wouldn't retrieve any entries with a blank name
// .toCollection.sortBy("name") didn't sort in an order I understood
const baseContacts = await db.contacts.toArray();
this.contacts = baseContacts.sort((a, b) =>
(a.name || "").localeCompare(b.name || ""),
const platformService = PlatformServiceFactory.getInstance();
const dbAllContacts = await platformService.dbQuery(
"SELECT * FROM contacts ORDER BY name",
);
this.contacts = databaseUtil.mapQueryResultToValues(
dbAllContacts,
) as unknown as Contact[];
if (USE_DEXIE_DB) {
await db.open();
// .orderBy("name") wouldn't retrieve any entries with a blank name
// .toCollection.sortBy("name") didn't sort in an order I understood
const baseContacts = await db.contacts.toArray();
this.contacts = baseContacts.sort((a, b) =>
(a.name || "").localeCompare(b.name || ""),
);
}
return;
}
@@ -929,7 +966,15 @@ export default class ContactsView extends Vue {
seesMe,
registered,
};
return db.contacts.add(newContact);
const platformService = PlatformServiceFactory.getInstance();
const { sql, params } = databaseUtil.generateInsertStatement(
newContact as unknown as Record<string, unknown>,
"contacts",
);
await platformService.dbExec(sql, params);
if (USE_DEXIE_DB) {
await db.contacts.add(newContact);
}
}
private async addContact(newContact: Contact) {
@@ -941,8 +986,20 @@ export default class ContactsView extends Vue {
this.danger("The DID must begin with 'did:'", "Invalid DID");
return;
}
return db.contacts
.add(newContact)
const platformService = PlatformServiceFactory.getInstance();
const { sql, params } = databaseUtil.generateInsertStatement(
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
contactPromise = db.contacts.add(newContact);
}
return contactPromise
.then(() => {
const allContacts = this.contacts.concat([newContact]);
this.contacts = R.sort(
@@ -970,17 +1027,27 @@ export default class ContactsView extends Vue {
text: "Do you want to register them?",
onCancel: async (stopAsking?: boolean) => {
if (stopAsking) {
await updateDefaultSettings({
await databaseUtil.updateDefaultSettings({
hideRegisterPromptOnNewContact: stopAsking,
});
if (USE_DEXIE_DB) {
await updateDefaultSettings({
hideRegisterPromptOnNewContact: stopAsking,
});
}
this.hideRegisterPromptOnNewContact = stopAsking;
}
},
onNo: async (stopAsking?: boolean) => {
if (stopAsking) {
await updateDefaultSettings({
await databaseUtil.updateDefaultSettings({
hideRegisterPromptOnNewContact: stopAsking,
});
if (USE_DEXIE_DB) {
await updateDefaultSettings({
hideRegisterPromptOnNewContact: stopAsking,
});
}
this.hideRegisterPromptOnNewContact = stopAsking;
}
},
@@ -1058,7 +1125,14 @@ export default class ContactsView extends Vue {
);
if (regResult.success) {
contact.registered = true;
await db.contacts.update(contact.did, { registered: true });
const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec(
"UPDATE contacts SET registered = ? WHERE did = ?",
[true, contact.did],
);
if (USE_DEXIE_DB) {
await db.contacts.update(contact.did, { registered: true });
}
this.$notify(
{
@@ -1267,9 +1341,14 @@ export default class ContactsView extends Vue {
private async toggleShowContactAmounts() {
const newShowValue = !this.showGiveNumbers;
try {
await updateDefaultSettings({
await databaseUtil.updateDefaultSettings({
showContactGivesInline: newShowValue,
});
if (USE_DEXIE_DB) {
await updateDefaultSettings({
showContactGivesInline: newShowValue,
});
}
} catch (err) {
const fullError =
"Error updating contact-amounts setting: " + errorStringForLog(err);

View File

@@ -232,10 +232,11 @@ import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import InfiniteScroll from "../components/InfiniteScroll.vue";
import TopMessage from "../components/TopMessage.vue";
import { NotificationIface } from "../constants/app";
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
import { Contact } from "../db/tables/contacts";
import { BoundingBox } from "../db/tables/settings";
import * as databaseUtil from "../db/databaseUtil";
import {
GenericCredWrapper,
GenericVerifiableCredential,
@@ -253,6 +254,7 @@ import {
import * as libsUtil from "../libs/util";
import EntityIcon from "../components/EntityIcon.vue";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
/**
* DIDView Component
@@ -323,7 +325,10 @@ export default class DIDView extends Vue {
* Initializes component settings from active account
*/
private async initializeSettings() {
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
}
@@ -370,7 +375,17 @@ export default class DIDView extends Vue {
private async loadContactInformation() {
if (!this.viewingDid) return;
this.contactFromDid = await db.contacts.get(this.viewingDid);
const platformService = PlatformServiceFactory.getInstance();
const dbContacts = await platformService.dbQuery(
"SELECT * FROM contacts WHERE did = ?",
[this.viewingDid],
);
this.contactFromDid = databaseUtil.mapQueryResultToValues(
dbContacts,
)[0] as unknown as Contact;
if (USE_DEXIE_DB) {
this.contactFromDid = await db.contacts.get(this.viewingDid);
}
if (this.contactFromDid) {
this.contactYaml = yaml.dump(this.contactFromDid);
}
@@ -433,8 +448,14 @@ export default class DIDView extends Vue {
* @param contact - Contact object to be deleted
*/
async deleteContact(contact: Contact) {
await db.open();
await db.contacts.delete(contact.did);
const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec("DELETE FROM contacts WHERE did = ?", [
contact.did,
]);
if (USE_DEXIE_DB) {
await db.open();
await db.contacts.delete(contact.did);
}
this.$notify(
{
group: "alert",
@@ -492,7 +513,14 @@ export default class DIDView extends Vue {
);
if (regResult.success) {
contact.registered = true;
await db.contacts.update(contact.did, { registered: true });
const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec(
"UPDATE contacts SET registered = ? WHERE did = ?",
[true, contact.did],
);
if (USE_DEXIE_DB) {
await db.contacts.update(contact.did, { registered: true });
}
this.$notify(
{
@@ -781,7 +809,14 @@ export default class DIDView extends Vue {
const visibility = resp.data;
contact.seesMe = visibility;
//console.log("Visi check:", visibility, contact.seesMe, contact.did);
await db.contacts.update(contact.did, { seesMe: visibility });
const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec(
"UPDATE contacts SET seesMe = ? WHERE did = ?",
[visibility, contact.did],
);
if (USE_DEXIE_DB) {
await db.contacts.update(contact.did, { seesMe: visibility });
}
this.$notify(
{

View File

@@ -42,7 +42,7 @@
import { computed, onMounted } from "vue";
import { useRoute, useRouter } from "vue-router";
import { VALID_DEEP_LINK_ROUTES } from "../interfaces/deepLinks";
import { logConsoleAndDb } from "../db";
import { logConsoleAndDb } from "../db/databaseUtil";
import { logger } from "../utils/logger";
const route = useRoute();

View File

@@ -322,6 +322,7 @@ import TopMessage from "../components/TopMessage.vue";
import {
NotificationIface,
DEFAULT_PARTNER_API_SERVER,
USE_DEXIE_DB,
} from "../constants/app";
import {
db,
@@ -330,6 +331,7 @@ import {
} from "../db/index";
import { Contact } from "../db/tables/contacts";
import { BoundingBox } from "../db/tables/settings";
import * as databaseUtil from "../db/databaseUtil";
import { PlanData } from "../interfaces";
import {
didInfo,
@@ -338,6 +340,8 @@ import {
} from "../libs/endorserServer";
import { OnboardPage, retrieveAccountDids } from "../libs/util";
import { logger } from "../utils/logger";
import { UserProfile } from "@/libs/partnerServer";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
interface Tile {
indexLat: number;
indexLon: number;
@@ -397,14 +401,22 @@ export default class DiscoverView extends Vue {
const searchPeople = !!this.$route.query["searchPeople"];
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.activeDid = (settings.activeDid as string) || "";
this.apiServer = (settings.apiServer as string) || "";
this.partnerApiServer =
(settings.partnerApiServer as string) || this.partnerApiServer;
this.searchBox = settings.searchBoxes?.[0] || null;
this.allContacts = await db.contacts.toArray();
const platformService = PlatformServiceFactory.getInstance();
const dbContacts = await platformService.dbQuery("SELECT * FROM contacts");
this.allContacts = databaseUtil.mapQueryResultToValues(dbContacts) as unknown as Contact[];
if (USE_DEXIE_DB) {
this.allContacts = await db.contacts.toArray();
}
this.allMyDids = await retrieveAccountDids();

View File

@@ -261,8 +261,13 @@ import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import ImageMethodDialog from "../components/ImageMethodDialog.vue";
import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
import {
DEFAULT_IMAGE_API_SERVER,
NotificationIface,
USE_DEXIE_DB,
} from "../constants/app";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
import * as databaseUtil from "../db/databaseUtil";
import { GenericCredWrapper, GiveVerifiableCredential } from "../interfaces";
import {
createAndSubmitGive,
@@ -275,6 +280,8 @@ import {
import * as libsUtil from "../libs/util";
import { retrieveAccountDids } from "../libs/util";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { Contact } from "@/db/tables/contacts";
@Component({
components: {
@@ -421,7 +428,10 @@ export default class GiftedDetails extends Vue {
this.imageUrl = this.$route.query["shareUrl"] as string;
}
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.apiServer = settings.apiServer || "";
this.activeDid = settings.activeDid || "";
@@ -429,7 +439,16 @@ export default class GiftedDetails extends Vue {
(this.giverDid && !this.giverName) ||
(this.recipientDid && !this.recipientName)
) {
const allContacts = await db.contacts.toArray();
const platformService = PlatformServiceFactory.getInstance();
const dbContacts = await platformService.dbQuery(
"SELECT * FROM contacts",
);
let allContacts = databaseUtil.mapQueryResultToValues(
dbContacts,
) as unknown as Contact[];
if (USE_DEXIE_DB) {
allContacts = await db.contacts.toArray();
}
const allMyDids = await retrieveAccountDids();
if (this.giverDid && !this.giverName) {
this.giverName = didInfo(

View File

@@ -308,13 +308,15 @@
import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "../components/QuickNav.vue";
import { NotificationIface } from "../constants/app";
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
import { DIRECT_PUSH_TITLE, sendTestThroughPushServer } from "../libs/util";
import PushNotificationPermission from "../components/PushNotificationPermission.vue";
import { db } from "../db/index";
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
import * as databaseUtil from "../db/databaseUtil";
import { Router } from "vue-router";
import { logger } from "../utils/logger";
@Component({ components: { PushNotificationPermission, QuickNav } })
export default class HelpNotificationsView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
@@ -420,10 +422,16 @@ export default class HelpNotificationsView extends Vue {
DIRECT_PUSH_TITLE,
async (success: boolean, timeText: string, message?: string) => {
if (success) {
await db.settings.update(MASTER_SETTINGS_KEY, {
databaseUtil.updateDefaultSettings({
notifyingReminderMessage: message,
notifyingReminderTime: timeText,
});
if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, {
notifyingReminderMessage: message,
notifyingReminderTime: timeText,
});
}
this.notifyingReminder = true;
this.notifyingReminderMessage = message || "";
this.notifyingReminderTime = timeText;

View File

@@ -114,5 +114,5 @@ import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "../components/QuickNav.vue";
@Component({ components: { QuickNav } })
export default class Help extends Vue {}
export default class HelpOnboardingView extends Vue {}
</script>

View File

@@ -579,14 +579,15 @@ import { useClipboard } from "@vueuse/core";
import * as Package from "../../package.json";
import QuickNav from "../components/QuickNav.vue";
import { NotificationIface } from "../constants/app";
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
import * as databaseUtil from "../db/databaseUtil";
import {
retrieveSettingsForActiveAccount,
updateAccountSettings,
} from "../db/index";
@Component({ components: { QuickNav } })
export default class Help extends Vue {
export default class HelpView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
@@ -609,11 +610,20 @@ export default class Help extends Vue {
}
async unsetFinishedOnboarding() {
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
if (settings.activeDid) {
await updateAccountSettings(settings.activeDid, {
await databaseUtil.updateAccountSettings(settings.activeDid, {
finishedOnboarding: false,
});
if (USE_DEXIE_DB) {
await updateAccountSettings(settings.activeDid, {
finishedOnboarding: false,
});
}
}
this.$router.push({ name: "home" });
}

View File

@@ -316,6 +316,7 @@ import {
AppString,
NotificationIface,
PASSKEYS_ENABLED,
USE_DEXIE_DB,
} from "../constants/app";
import {
db,
@@ -329,6 +330,7 @@ import {
checkIsAnyFeedFilterOn,
MASTER_SETTINGS_KEY,
} from "../db/tables/settings";
import * as databaseUtil from "../db/databaseUtil";
import {
contactForDid,
containsNonHiddenDid,
@@ -349,6 +351,7 @@ import { GiveSummaryRecord } from "../interfaces";
import * as serverUtil from "../libs/endorserServer";
import { logger } from "../utils/logger";
import { GiveRecordWithContactInfo } from "../types";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
interface Claim {
claim?: Claim; // For nested claims in Verifiable Credentials
@@ -512,18 +515,92 @@ export default class HomeView extends Vue {
*/
private async initializeIdentity() {
try {
this.allMyDids = await retrieveAccountDids();
if (this.allMyDids.length === 0) {
this.isCreatingIdentifier = true;
const newDid = await generateSaveAndActivateIdentity();
this.isCreatingIdentifier = false;
this.allMyDids = [newDid];
// Retrieve DIDs with better error handling
try {
this.allMyDids = await retrieveAccountDids();
logConsoleAndDb(`[HomeView] Retrieved ${this.allMyDids.length} DIDs`);
} catch (error) {
logConsoleAndDb(`[HomeView] Failed to retrieve DIDs: ${error}`, true);
throw new Error(
"Failed to load existing identities. Please try restarting the app.",
);
}
const settings = await retrieveSettingsForActiveAccount();
// Create new DID if needed
if (this.allMyDids.length === 0) {
try {
this.isCreatingIdentifier = true;
const newDid = await generateSaveAndActivateIdentity();
this.isCreatingIdentifier = false;
this.allMyDids = [newDid];
logConsoleAndDb(`[HomeView] Created new identity: ${newDid}`);
} catch (error) {
this.isCreatingIdentifier = false;
logConsoleAndDb(
`[HomeView] Failed to create new identity: ${error}`,
true,
);
throw new Error("Failed to create new identity. Please try again.");
}
}
// Load settings with better error context
let settings;
try {
settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
logConsoleAndDb(
`[HomeView] Retrieved settings for ${settings.activeDid || "no active DID"}`,
);
} catch (error) {
logConsoleAndDb(
`[HomeView] Failed to retrieve settings: ${error}`,
true,
);
throw new Error(
"Failed to load user settings. Some features may be limited.",
);
}
// Update component state
this.apiServer = settings.apiServer || "";
this.activeDid = settings.activeDid || "";
this.allContacts = await db.contacts.toArray();
// Load contacts with graceful fallback
try {
const platformService = PlatformServiceFactory.getInstance();
const dbContacts = await platformService.dbQuery(
"SELECT * FROM contacts",
);
this.allContacts = databaseUtil.mapQueryResultToValues(
dbContacts,
) as Contact[];
if (USE_DEXIE_DB) {
this.allContacts = await db.contacts.toArray();
}
logConsoleAndDb(
`[HomeView] Retrieved ${this.allContacts.length} contacts`,
);
} catch (error) {
logConsoleAndDb(
`[HomeView] Failed to retrieve contacts: ${error}`,
true,
);
this.allContacts = []; // Ensure we have a valid empty array
this.$notify(
{
group: "alert",
type: "warning",
title: "Contact Loading Issue",
text: "Some contact information may be unavailable.",
},
5000,
);
}
// Update remaining settings
this.feedLastViewedClaimId = settings.lastViewedClaimId;
this.givenName = settings.firstName || "";
this.isFeedFilteredByVisible = !!settings.filterFeedByVisible;
@@ -534,16 +611,16 @@ export default class HomeView extends Vue {
settings.lastAckedOfferToUserProjectsJwtId;
this.searchBoxes = settings.searchBoxes || [];
this.showShortcutBvc = !!settings.showShortcutBvc;
this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings);
// Check onboarding status
if (!settings.finishedOnboarding) {
(this.$refs.onboardingDialog as OnboardingDialog).open(
OnboardPage.Home,
);
}
// someone may have have registered after sharing contact info, so recheck
// Check registration status if needed
if (!this.isRegistered && this.activeDid) {
try {
const resp = await fetchEndorserRateLimits(
@@ -552,56 +629,86 @@ export default class HomeView extends Vue {
this.activeDid,
);
if (resp.status === 200) {
await updateAccountSettings(this.activeDid, {
await databaseUtil.updateAccountSettings(this.activeDid, {
isRegistered: true,
...(await retrieveSettingsForActiveAccount()),
...(await databaseUtil.retrieveSettingsForActiveAccount()),
});
if (USE_DEXIE_DB) {
await updateAccountSettings(this.activeDid, {
isRegistered: true,
...(await retrieveSettingsForActiveAccount()),
});
}
this.isRegistered = true;
logConsoleAndDb(
`[HomeView] User ${this.activeDid} is now registered`,
);
}
} catch (e) {
// ignore the error... just keep us unregistered
} catch (error) {
logConsoleAndDb(
`[HomeView] Registration check failed: ${error}`,
true,
);
// Continue as unregistered - this is expected for new users
}
}
// this returns a Promise but we don't need to wait for it
this.updateAllFeed();
// Initialize feed and offers
try {
// Start feed update in background
this.updateAllFeed().catch((error) => {
logConsoleAndDb(
`[HomeView] Background feed update failed: ${error}`,
true,
);
});
if (this.activeDid) {
const offersToUserData = await getNewOffersToUser(
this.axios,
this.apiServer,
this.activeDid,
this.lastAckedOfferToUserJwtId,
// Load new offers if we have an active DID
if (this.activeDid) {
const [offersToUser, offersToProjects] = await Promise.all([
getNewOffersToUser(
this.axios,
this.apiServer,
this.activeDid,
this.lastAckedOfferToUserJwtId,
),
getNewOffersToUserProjects(
this.axios,
this.apiServer,
this.activeDid,
this.lastAckedOfferToUserProjectsJwtId,
),
]);
this.numNewOffersToUser = offersToUser.data.length;
this.newOffersToUserHitLimit = offersToUser.hitLimit;
this.numNewOffersToUserProjects = offersToProjects.data.length;
this.newOffersToUserProjectsHitLimit = offersToProjects.hitLimit;
logConsoleAndDb(
`[HomeView] Retrieved ${this.numNewOffersToUser} user offers and ` +
`${this.numNewOffersToUserProjects} project offers`,
);
}
} catch (error) {
logConsoleAndDb(
`[HomeView] Failed to initialize feed/offers: ${error}`,
true,
);
this.numNewOffersToUser = offersToUserData.data.length;
this.newOffersToUserHitLimit = offersToUserData.hitLimit;
}
if (this.activeDid) {
const offersToUserProjects = await getNewOffersToUserProjects(
this.axios,
this.apiServer,
this.activeDid,
this.lastAckedOfferToUserProjectsJwtId,
// Don't throw - we can continue with empty feed
this.$notify(
{
group: "alert",
type: "warning",
title: "Feed Loading Issue",
text: "Some feed data may be unavailable. Pull to refresh.",
},
5000,
);
this.numNewOffersToUserProjects = offersToUserProjects.data.length;
this.newOffersToUserProjectsHitLimit = offersToUserProjects.hitLimit;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
logConsoleAndDb("Error retrieving settings or feed: " + err, true);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text:
(err as { userMessage?: string })?.userMessage ||
"There was an error retrieving your settings or the latest activity.",
},
5000,
);
} catch (error) {
this.handleError(error);
throw error; // Re-throw to be caught by mounted()
}
}
@@ -618,7 +725,10 @@ export default class HomeView extends Vue {
* Called by mounted() and reloadFeedOnChange()
*/
private async loadSettings() {
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.apiServer = settings.apiServer || "";
this.activeDid = settings.activeDid || "";
this.feedLastViewedClaimId = settings.lastViewedClaimId;
@@ -642,7 +752,14 @@ export default class HomeView extends Vue {
* Called by mounted() and initializeIdentity()
*/
private async loadContacts() {
this.allContacts = await db.contacts.toArray();
const platformService = PlatformServiceFactory.getInstance();
const dbContacts = await platformService.dbQuery("SELECT * FROM contacts");
this.allContacts = databaseUtil.mapQueryResultToValues(
dbContacts,
) as unknown as Contact[];
if (USE_DEXIE_DB) {
this.allContacts = await db.contacts.toArray();
}
}
/**
@@ -663,11 +780,22 @@ export default class HomeView extends Vue {
this.activeDid,
);
if (resp.status === 200) {
await updateAccountSettings(this.activeDid, {
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
await databaseUtil.updateAccountSettings(this.activeDid, {
apiServer: this.apiServer,
isRegistered: true,
...(await retrieveSettingsForActiveAccount()),
...settings,
});
if (USE_DEXIE_DB) {
await updateAccountSettings(this.activeDid, {
apiServer: this.apiServer,
isRegistered: true,
...settings,
});
}
this.isRegistered = true;
}
} catch (e) {
@@ -728,7 +856,10 @@ export default class HomeView extends Vue {
* Called by mounted()
*/
private async checkOnboarding() {
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
if (!settings.finishedOnboarding) {
(this.$refs.onboardingDialog as OnboardingDialog).open(OnboardPage.Home);
}
@@ -740,19 +871,26 @@ export default class HomeView extends Vue {
* - Displays user notification
*
* @internal
* Called by mounted()
* Called by mounted() and initializeIdentity()
* @param err Error object with optional userMessage
*/
private handleError(err: unknown) {
logConsoleAndDb("Error retrieving settings or feed: " + err, true);
const errorMessage = err instanceof Error ? err.message : String(err);
const userMessage = (err as { userMessage?: string })?.userMessage;
logConsoleAndDb(
`[HomeView] Initialization error: ${errorMessage}${userMessage ? ` (${userMessage})` : ""}`,
true,
);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text:
(err as { userMessage?: string })?.userMessage ||
"There was an error retrieving your settings or the latest activity.",
userMessage ||
"There was an error loading your data. Please try refreshing the page.",
},
5000,
);
@@ -790,7 +928,10 @@ export default class HomeView extends Vue {
* Called by FeedFilters component when filters change
*/
async reloadFeedOnChange() {
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.isFeedFilteredByVisible = !!settings.filterFeedByVisible;
this.isFeedFilteredByNearby = !!settings.filterFeedByNearby;
this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings);
@@ -1230,10 +1371,15 @@ export default class HomeView extends Vue {
this.feedLastViewedClaimId == null ||
this.feedLastViewedClaimId < records[0].jwtId
) {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
await databaseUtil.updateDefaultSettings({
lastViewedClaimId: records[0].jwtId,
});
if (USE_DEXIE_DB) {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
lastViewedClaimId: records[0].jwtId,
});
}
}
}

View File

@@ -105,15 +105,18 @@ import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import { NotificationIface } from "../constants/app";
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
import {
accountsDBPromise,
db,
retrieveSettingsForActiveAccount,
} from "../db/index";
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
import * as databaseUtil from "../db/databaseUtil";
import { retrieveAllAccountsMetadata } from "../libs/util";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
@Component({ components: { QuickNav } })
export default class IdentitySwitcherView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
@@ -127,7 +130,10 @@ export default class IdentitySwitcherView extends Vue {
async created() {
try {
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.apiServerInput = settings.apiServer || "";
@@ -162,10 +168,17 @@ export default class IdentitySwitcherView extends Vue {
if (did === "0") {
did = undefined;
}
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: did,
});
const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec(
`UPDATE settings SET activeDid = ? WHERE id = ?`,
[did ?? "", MASTER_SETTINGS_KEY],
);
if (USE_DEXIE_DB) {
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: did ?? "",
});
}
this.$router.push({ name: "account" });
}

View File

@@ -86,20 +86,21 @@
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { AppString, NotificationIface } from "../constants/app";
import { AppString, NotificationIface, USE_DEXIE_DB } from "../constants/app";
import * as databaseUtil from "../db/databaseUtil";
import {
accountsDBPromise,
db,
retrieveSettingsForActiveAccount,
} from "../db/index";
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
import {
DEFAULT_ROOT_DERIVATION_PATH,
deriveAddress,
newIdentifier,
} from "../libs/crypto";
import { retrieveAccountCount } from "../libs/util";
import { retrieveAccountCount, saveNewIdentity } from "../libs/util";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { logger } from "../utils/logger";
@Component({
components: {},
})
@@ -126,7 +127,10 @@ export default class ImportAccountView extends Vue {
async created() {
this.numAccounts = await retrieveAccountCount();
// get the server, to help with import on the test server
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.apiServer = settings.apiServer || "";
}
@@ -155,22 +159,13 @@ export default class ImportAccountView extends Vue {
const accountsDB = await accountsDBPromise;
if (this.shouldErase) {
await accountsDB.accounts.clear();
const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec("DELETE FROM accounts");
if (USE_DEXIE_DB) {
await accountsDB.accounts.clear();
}
}
await accountsDB.accounts.add({
dateCreated: new Date().toISOString(),
derivationPath: this.derivationPath,
did: newId.did,
identity: JSON.stringify(newId),
mnemonic: mne,
publicKeyHex: newId.keys[0].publicKeyHex,
});
// record that as the active DID
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: newId.did,
});
saveNewIdentity(JSON.stringify(newId), mne, newId, this.derivationPath);
this.$router.push({ name: "account" });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {

View File

@@ -80,8 +80,12 @@ import {
} from "../libs/crypto";
import { accountsDBPromise, db } from "../db/index";
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
import { retrieveAllFullyDecryptedAccounts } from "../libs/util";
import * as databaseUtil from "../db/databaseUtil";
import { retrieveAllAccountsMetadata } from "../libs/util";
import { logger } from "../utils/logger";
import { Account } from "../db/tables/accounts";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { USE_DEXIE_DB } from "@/constants/app";
@Component({
components: {},
})
@@ -94,14 +98,20 @@ export default class ImportAccountView extends Vue {
selectedArrayFirstDid = "";
async mounted() {
const accounts = await retrieveAllFullyDecryptedAccounts(); // let's match derived accounts differently so we don't need the private info
const accounts: Account[] = await retrieveAllAccountsMetadata();
const seedDids: Record<string, Array<string>> = {};
accounts.forEach((account) => {
const prevDids: Array<string> = seedDids[account.mnemonic] || [];
seedDids[account.mnemonic] = prevDids.concat([account.did]);
// Since we're only getting metadata, we can't check mnemonic
// Instead, we'll group by derivation path
if (account.derivationPath) {
const prevDids: Array<string> = seedDids[account.derivationPath] || [];
seedDids[account.derivationPath] = prevDids.concat([account.did]);
}
});
this.didArrays = Object.values(seedDids);
this.selectedArrayFirstDid = this.didArrays[0][0];
if (this.didArrays.length > 0) {
this.selectedArrayFirstDid = this.didArrays[0][0];
}
}
public onCancelClick() {
@@ -117,14 +127,29 @@ export default class ImportAccountView extends Vue {
const selectedArray: Array<string> =
this.didArrays.find((dids) => dids[0] === this.selectedArrayFirstDid) ||
[];
const accountsDB = await accountsDBPromise; // let's match derived accounts differently so we don't need the private info
const allMatchingAccounts = await accountsDB.accounts
.where("did")
.anyOf(...selectedArray)
.toArray();
const platformService = PlatformServiceFactory.getInstance();
const qmarks = selectedArray.map(() => "?").join(",");
const queryResult = await platformService.dbQuery(
`SELECT * FROM accounts WHERE did IN (${qmarks})`,
selectedArray,
);
let allMatchingAccounts = databaseUtil.mapQueryResultToValues(
queryResult,
) as unknown as Account[];
if (USE_DEXIE_DB) {
const accountsDB = await accountsDBPromise; // let's match derived accounts differently so we don't need the private info
allMatchingAccounts = (await accountsDB.accounts
.where("did")
.anyOf(...selectedArray)
.toArray()) as Account[];
}
const accountWithMaxDeriv = allMatchingAccounts[0];
allMatchingAccounts.slice(1).forEach((account) => {
if (account.derivationPath > accountWithMaxDeriv.derivationPath) {
if (
account.derivationPath &&
accountWithMaxDeriv.derivationPath &&
account.derivationPath > accountWithMaxDeriv.derivationPath
) {
accountWithMaxDeriv.derivationPath = account.derivationPath;
}
});
@@ -140,20 +165,40 @@ export default class ImportAccountView extends Vue {
const newId = newIdentifier(address, publicHex, privateHex, newDerivPath);
try {
await accountsDB.accounts.add({
dateCreated: new Date().toISOString(),
derivationPath: newDerivPath,
did: newId.did,
identity: JSON.stringify(newId),
mnemonic: mne,
publicKeyHex: newId.keys[0].publicKeyHex,
});
const { sql, params } = databaseUtil.generateInsertStatement(
{
dateCreated: new Date().toISOString(),
derivationPath: newDerivPath,
did: newId.did,
identity: JSON.stringify(newId),
mnemonic: mne,
publicKeyHex: newId.keys[0].publicKeyHex,
},
"accounts",
);
const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec(sql, params);
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
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: newId.did,
});
await platformService.dbExec("UPDATE settings SET activeDid = ?", [
newId.did,
]);
if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: newId.did,
});
}
this.$router.push({ name: "account" });
} catch (err) {
logger.error("Error saving mnemonic & updating settings:", err);

View File

@@ -42,12 +42,13 @@ import { Component, Vue } from "vue-facing-decorator";
import { Router, RouteLocationNormalized } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import { APP_SERVER, NotificationIface } from "../constants/app";
import { APP_SERVER, NotificationIface, USE_DEXIE_DB } from "../constants/app";
import {
db,
logConsoleAndDb,
retrieveSettingsForActiveAccount,
} from "../db/index";
import * as databaseUtil from "../db/databaseUtil";
import { decodeEndorserJwt } from "../libs/crypto/vc";
import { errorStringForLog } from "../libs/endorserServer";
import { generateSaveAndActivateIdentity } from "../libs/util";
@@ -112,10 +113,13 @@ export default class InviteOneAcceptView extends Vue {
*/
async mounted() {
this.checkingInvite = true;
await db.open();
// Load or generate identity
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
await db.open();
settings = await retrieveSettingsForActiveAccount();
}
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";

View File

@@ -138,11 +138,19 @@ import ContactNameDialog from "../components/ContactNameDialog.vue";
import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
import InviteDialog from "../components/InviteDialog.vue";
import { APP_SERVER, AppString, NotificationIface } from "../constants/app";
import {
APP_SERVER,
AppString,
NotificationIface,
USE_DEXIE_DB,
} from "../constants/app";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
import { Contact } from "../db/tables/contacts";
import * as databaseUtil from "../db/databaseUtil";
import { createInviteJwt, getHeaders } from "../libs/endorserServer";
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
import { logger } from "../utils/logger";
interface Invite {
inviteIdentifier: string;
expiresAt: string;
@@ -168,8 +176,11 @@ export default class InviteOneView extends Vue {
async mounted() {
try {
await db.open();
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
await db.open();
settings = await retrieveSettingsForActiveAccount();
}
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.isRegistered = !!settings.isRegistered;
@@ -181,7 +192,16 @@ export default class InviteOneView extends Vue {
);
this.invites = response.data.data;
const baseContacts: Contact[] = await db.contacts.toArray();
const platformService = PlatformServiceFactory.getInstance();
const queryResult = await platformService.dbQuery(
"SELECT * FROM contacts",
);
let baseContacts = databaseUtil.mapQueryResultToValues(
queryResult,
) as unknown as Contact[];
if (USE_DEXIE_DB) {
baseContacts = await db.contacts.toArray();
}
for (const invite of this.invites) {
const contact = baseContacts.find(
(contact) => contact.did === invite.redeemedBy,
@@ -328,18 +348,27 @@ export default class InviteOneView extends Vue {
);
}
addNewContact(did: string, notes: string) {
async addNewContact(did: string, notes: string) {
(this.$refs.contactNameDialog as ContactNameDialog).open(
"To Whom Did You Send The Invite?",
"Their name will be added to your contact list.",
(name) => {
async (name) => {
// the person obviously registered themselves and this user already granted visibility, so we just add them
const contact = {
did: did,
name: name,
registered: true,
};
db.contacts.add(contact);
const platformService = PlatformServiceFactory.getInstance();
const columns = Object.keys(contact);
const values = Object.values(contact);
const placeholders = values.map(() => "?").join(", ");
const sql = `INSERT INTO contacts (${columns.join(", ")}) VALUES (${placeholders})`;
await platformService.dbExec(sql, values);
if (USE_DEXIE_DB) {
await db.contacts.add(contact);
}
this.contactsRedeemed[did] = contact;
this.$notify(
{

View File

@@ -35,14 +35,20 @@
No logs found.
</div>
<div v-else>
<div v-for="(log, index) in logs" :key="index" class="mb-8">
<h2 class="text-lg font-semibold mb-2">{{ log.date }}</h2>
<div v-for="(log, index) in logs" :key="index" class="mb-2">
<pre
class="bg-slate-100 p-4 rounded-md overflow-x-auto whitespace-pre-wrap"
>{{ log.message }}</pre
>{{ log.date }} {{ log.message }}</pre
>
</div>
</div>
<div class="text-slate-500">
<h2 class="text-lg font-bold mb-2">Memory Logs</h2>
<pre
class="bg-slate-100 p-4 rounded-md overflow-x-auto whitespace-pre-wrap"
>{{ memoryLogs.join("\n") }}</pre
>
</div>
</section>
</template>
@@ -55,6 +61,9 @@ import TopMessage from "../components/TopMessage.vue";
import { db } from "../db/index";
import { Log } from "../db/tables/logs";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
import { USE_DEXIE_DB } from "../constants/app";
import * as databaseUtil from "../db/databaseUtil";
@Component({
components: {
@@ -68,6 +77,7 @@ export default class LogView extends Vue {
loading = true;
logs: Log[] = [];
error: string | null = null;
memoryLogs: string[] = [];
async mounted() {
await this.loadLogs();
@@ -76,14 +86,26 @@ export default class LogView extends Vue {
async loadLogs() {
try {
this.error = null; // Clear any previous errors
await db.open();
// Get all logs and sort by date in reverse chronological order
const allLogs = await db.logs.toArray();
this.logs = allLogs.sort((a, b) => {
const dateA = new Date(a.date);
const dateB = new Date(b.date);
return dateB.getTime() - dateA.getTime();
});
this.memoryLogs = databaseUtil.memoryLogs;
let allLogs: Log[] = [];
const platformService = PlatformServiceFactory.getInstance();
const queryResult = await platformService.dbQuery(
"SELECT * FROM logs ORDER BY date DESC",
);
this.logs = databaseUtil.mapQueryResultToValues(
queryResult,
) as unknown as Log[];
if (USE_DEXIE_DB) {
await db.open();
allLogs = await db.logs.toArray();
// Sort by date in reverse chronological order
this.logs = allLogs.sort((a, b) => {
const dateA = new Date(a.date);
const dateB = new Date(b.date);
return dateB.getTime() - dateA.getTime();
});
}
} catch (error) {
logger.error("Error loading logs:", error);
this.error =

View File

@@ -153,7 +153,7 @@ import { Component, Vue } from "vue-facing-decorator";
import GiftedDialog from "../components/GiftedDialog.vue";
import QuickNav from "../components/QuickNav.vue";
import EntityIcon from "../components/EntityIcon.vue";
import { NotificationIface } from "../constants/app";
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
import {
db,
retrieveSettingsForActiveAccount,
@@ -169,6 +169,9 @@ import {
getNewOffersToUserProjects,
} from "../libs/endorserServer";
import { retrieveAccountDids } from "../libs/util";
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
import * as databaseUtil from "../db/databaseUtil";
import { logger } from "../utils/logger";
@Component({
components: { GiftedDialog, QuickNav, EntityIcon },
@@ -194,14 +197,28 @@ export default class NewActivityView extends Vue {
async created() {
try {
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.apiServer = settings.apiServer || "";
this.activeDid = settings.activeDid || "";
this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId || "";
this.lastAckedOfferToUserProjectsJwtId =
settings.lastAckedOfferToUserProjectsJwtId || "";
this.allContacts = await db.contacts.toArray();
const platformService = PlatformServiceFactory.getInstance();
const queryResult = await platformService.dbQuery(
"SELECT * FROM contacts",
);
this.allContacts = databaseUtil.mapQueryResultToValues(
queryResult,
) as unknown as Contact[];
if (USE_DEXIE_DB) {
await db.open();
this.allContacts = await db.contacts.toArray();
}
this.allMyDids = await retrieveAccountDids();
const offersToUserData = await getNewOffersToUser(
@@ -240,9 +257,14 @@ export default class NewActivityView extends Vue {
async expandOffersToUserAndMarkRead() {
this.showOffersDetails = !this.showOffersDetails;
if (this.showOffersDetails) {
await updateAccountSettings(this.activeDid, {
await databaseUtil.updateAccountSettings(this.activeDid, {
lastAckedOfferToUserJwtId: this.newOffersToUser[0].jwtId,
});
if (USE_DEXIE_DB) {
await updateAccountSettings(this.activeDid, {
lastAckedOfferToUserJwtId: this.newOffersToUser[0].jwtId,
});
}
// note that we don't update this.lastAckedOfferToUserJwtId in case they
// later choose the last one to keep the offers as new
this.$notify(
@@ -263,14 +285,24 @@ export default class NewActivityView extends Vue {
);
if (index !== -1 && index < this.newOffersToUser.length - 1) {
// Set to the next offer's jwtId
await updateAccountSettings(this.activeDid, {
await databaseUtil.updateAccountSettings(this.activeDid, {
lastAckedOfferToUserJwtId: this.newOffersToUser[index + 1].jwtId,
});
if (USE_DEXIE_DB) {
await db.settings.update(this.activeDid, {
lastAckedOfferToUserJwtId: this.newOffersToUser[index + 1].jwtId,
});
}
} else {
// it's the last entry (or not found), so just keep it the same
await updateAccountSettings(this.activeDid, {
await databaseUtil.updateAccountSettings(this.activeDid, {
lastAckedOfferToUserJwtId: this.lastAckedOfferToUserJwtId,
});
if (USE_DEXIE_DB) {
await db.settings.update(this.activeDid, {
lastAckedOfferToUserJwtId: this.lastAckedOfferToUserJwtId,
});
}
}
this.$notify(
{
@@ -287,10 +319,16 @@ export default class NewActivityView extends Vue {
this.showOffersToUserProjectsDetails =
!this.showOffersToUserProjectsDetails;
if (this.showOffersToUserProjectsDetails) {
await updateAccountSettings(this.activeDid, {
await databaseUtil.updateAccountSettings(this.activeDid, {
lastAckedOfferToUserProjectsJwtId:
this.newOffersToUserProjects[0].jwtId,
});
if (USE_DEXIE_DB) {
await db.settings.update(this.activeDid, {
lastAckedOfferToUserProjectsJwtId:
this.newOffersToUserProjects[0].jwtId,
});
}
// note that we don't update this.lastAckedOfferToUserProjectsJwtId in case
// they later choose the last one to keep the offers as new
this.$notify(
@@ -311,16 +349,28 @@ export default class NewActivityView extends Vue {
);
if (index !== -1 && index < this.newOffersToUserProjects.length - 1) {
// Set to the next offer's jwtId
await updateAccountSettings(this.activeDid, {
await databaseUtil.updateAccountSettings(this.activeDid, {
lastAckedOfferToUserProjectsJwtId:
this.newOffersToUserProjects[index + 1].jwtId,
});
if (USE_DEXIE_DB) {
await db.settings.update(this.activeDid, {
lastAckedOfferToUserProjectsJwtId:
this.newOffersToUserProjects[index + 1].jwtId,
});
}
} else {
// it's the last entry (or not found), so just keep it the same
await updateAccountSettings(this.activeDid, {
await databaseUtil.updateAccountSettings(this.activeDid, {
lastAckedOfferToUserProjectsJwtId:
this.lastAckedOfferToUserProjectsJwtId,
});
if (USE_DEXIE_DB) {
await db.settings.update(this.activeDid, {
lastAckedOfferToUserProjectsJwtId:
this.lastAckedOfferToUserProjectsJwtId,
});
}
}
this.$notify(
{

View File

@@ -47,8 +47,10 @@
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { USE_DEXIE_DB } from "@/constants/app";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
import * as databaseUtil from "../db/databaseUtil";
@Component({
components: {},
@@ -60,17 +62,26 @@ export default class NewEditAccountView extends Vue {
// 'created' hook runs when the Vue instance is first created
async created() {
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.givenName =
(settings.firstName || "") +
(settings.lastName ? ` ${settings.lastName}` : ""); // deprecated, pre v 0.1.3
}
async onClickSaveChanges() {
await db.settings.update(MASTER_SETTINGS_KEY, {
await databaseUtil.updateDefaultSettings({
firstName: this.givenName,
lastName: "", // deprecated, pre v 0.1.3
});
if (USE_DEXIE_DB) {
await db.settings.update(MASTER_SETTINGS_KEY, {
firstName: this.givenName,
lastName: "", // deprecated, pre v 0.1.3
});
}
this.$router.back();
}

View File

@@ -237,7 +237,9 @@ import {
DEFAULT_IMAGE_API_SERVER,
DEFAULT_PARTNER_API_SERVER,
NotificationIface,
USE_DEXIE_DB,
} from "../constants/app";
import * as databaseUtil from "../db/databaseUtil";
import { retrieveSettingsForActiveAccount } from "../db/index";
import { PlanVerifiableCredential } from "../interfaces";
import {
@@ -303,7 +305,10 @@ export default class NewEditProjectView extends Vue {
async mounted() {
this.numAccounts = await retrieveAccountCount();
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.showGeneralAdvanced = !!settings.showGeneralAdvanced;
@@ -702,7 +707,10 @@ export default class NewEditProjectView extends Vue {
) {
try {
let partnerServer = DEFAULT_PARTNER_API_SERVER;
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
if (settings.partnerApiServer) {
partnerServer = settings.partnerApiServer;
}

View File

@@ -69,6 +69,7 @@ import { Router } from "vue-router";
import { generateSaveAndActivateIdentity } from "../libs/util";
import QuickNav from "../components/QuickNav.vue";
import { logger } from "../utils/logger";
@Component({ components: { QuickNav } })
export default class NewIdentifierView extends Vue {
@@ -89,7 +90,7 @@ export default class NewIdentifierView extends Vue {
.catch((error) => {
this.loading = false;
this.hitError = true;
console.error("Failed to generate identity:", error);
logger.error("Failed to generate identity:", error);
});
}
}

View File

@@ -180,7 +180,7 @@ import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
import { NotificationIface } from "../constants/app";
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
import { GenericCredWrapper, OfferVerifiableCredential } from "../interfaces";
import {
@@ -193,6 +193,10 @@ import {
import * as libsUtil from "../libs/util";
import { retrieveAccountDids } from "../libs/util";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { Contact } from "@/db/tables/contacts";
import * as databaseUtil from "../db/databaseUtil";
/**
* Offer Details View Component
* @author Matthew Raymer
@@ -398,7 +402,10 @@ export default class OfferDetailsView extends Vue {
* @throws Will not throw but logs errors
*/
private async loadAccountSettings() {
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.apiServer = settings.apiServer ?? "";
this.activeDid = settings.activeDid ?? "";
this.showGeneralAdvanced = settings.showGeneralAdvanced ?? false;
@@ -409,7 +416,18 @@ export default class OfferDetailsView extends Vue {
*/
private async loadRecipientInfo() {
if (this.recipientDid && !this.recipientName) {
const allContacts = await db.contacts.toArray();
let allContacts: Contact[] = [];
const platformService = PlatformServiceFactory.getInstance();
const queryResult = await platformService.dbQuery(
"SELECT * FROM contacts",
);
allContacts = databaseUtil.mapQueryResultToValues(
queryResult,
) as unknown as Contact[];
if (USE_DEXIE_DB) {
await db.open();
allContacts = await db.contacts.toArray();
}
const allMyDids = await retrieveAccountDids();
this.recipientName = didInfo(
this.recipientDid,

View File

@@ -89,13 +89,16 @@ import { Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from "../db/index";
import * as databaseUtil from "../db/databaseUtil";
import { retrieveSettingsForActiveAccount } from "../db/index";
import { logConsoleAndDb } from "../db/databaseUtil";
import {
errorStringForLog,
getHeaders,
serverMessageForUser,
} from "../libs/endorserServer";
import { encryptMessage } from "../libs/crypto";
import { USE_DEXIE_DB } from "@/constants/app";
interface Meeting {
name: string;
@@ -134,7 +137,10 @@ export default class OnboardMeetingListView extends Vue {
showPasswordDialog = false;
async created() {
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.firstName = settings.firstName || "";

View File

@@ -55,7 +55,9 @@ import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
import MembersList from "../components/MembersList.vue";
import UserNameDialog from "../components/UserNameDialog.vue";
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from "../db/index";
import * as databaseUtil from "../db/databaseUtil";
import { retrieveSettingsForActiveAccount } from "../db/index";
import { logConsoleAndDb } from "../db/databaseUtil";
import { encryptMessage } from "../libs/crypto";
import {
errorStringForLog,
@@ -63,6 +65,7 @@ import {
serverMessageForUser,
} from "../libs/endorserServer";
import { generateSaveAndActivateIdentity } from "../libs/util";
import { USE_DEXIE_DB } from "@/constants/app";
@Component({
components: {
@@ -104,7 +107,10 @@ export default class OnboardMeetingMembersView extends Vue {
this.isLoading = false;
return;
}
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.firstName = settings.firstName || "";

View File

@@ -273,7 +273,9 @@ import { useClipboard } from "@vueuse/core";
import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
import MembersList from "../components/MembersList.vue";
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from "../db/index";
import * as databaseUtil from "../db/databaseUtil";
import { retrieveSettingsForActiveAccount } from "../db/index";
import { logConsoleAndDb } from "../db/databaseUtil";
import {
errorStringForLog,
getHeaders,
@@ -281,7 +283,7 @@ import {
} from "../libs/endorserServer";
import { encryptMessage } from "../libs/crypto";
import { logger } from "../utils/logger";
import { USE_DEXIE_DB } from "@/constants/app";
interface ServerMeeting {
groupId: number; // from the server
name: string; // to & from the server
@@ -328,7 +330,10 @@ export default class OnboardMeetingView extends Vue {
}
async created() {
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.fullName = settings.firstName || "";

View File

@@ -386,11 +386,15 @@
>
<!-- just show the hours, or alternatively whatever is first -->
<span v-if="givenTotalHours() > 0">
{{ givenTotalHours() }} {{ libsUtil.UNIT_SHORT["HUR"] }}
{{ libsUtil.formattedAmount(givenTotalHours(), "HUR") }}
</span>
<span v-else>
{{ givesTotalsByUnit[0].amount }}
{{ libsUtil.UNIT_SHORT[givesTotalsByUnit[0].unit] }}
{{
libsUtil.formattedAmount(
givesTotalsByUnit[0].amount,
givesTotalsByUnit[0].unit,
)
}}
</span>
<span v-if="givesTotalsByUnit.length > 1">...</span>
<span>
@@ -411,7 +415,7 @@
:icon="libsUtil.iconForUnitCode(total.unit)"
class="fa-fw text-slate-400 mr-1"
/>
{{ total.amount }} {{ libsUtil.UNIT_LONG[total.unit] }}
{{ libsUtil.formattedAmount(total.amount, total.unit) }}
</div>
</div>
</span>
@@ -624,7 +628,8 @@ import TopMessage from "../components/TopMessage.vue";
import QuickNav from "../components/QuickNav.vue";
import EntityIcon from "../components/EntityIcon.vue";
import ProjectIcon from "../components/ProjectIcon.vue";
import { NotificationIface } from "../constants/app";
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
import * as databaseUtil from "../db/databaseUtil";
import {
db,
logConsoleAndDb,
@@ -636,6 +641,7 @@ import * as serverUtil from "../libs/endorserServer";
import { retrieveAccountDids } from "../libs/util";
import HiddenDidDialog from "../components/HiddenDidDialog.vue";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
/**
* Project View Component
* @author Matthew Raymer
@@ -778,10 +784,21 @@ export default class ProjectViewView extends Vue {
* @emits Notification on profile loading errors
*/
async created() {
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.allContacts = await db.contacts.toArray();
const platformService = PlatformServiceFactory.getInstance();
const queryResult = await platformService.dbQuery("SELECT * FROM contacts");
this.allContacts = databaseUtil.mapQueryResultToValues(
queryResult,
) as unknown as Contact[];
if (USE_DEXIE_DB) {
await db.open();
this.allContacts = await db.contacts.toArray();
}
this.isRegistered = !!settings.isRegistered;
try {

View File

@@ -269,7 +269,7 @@ import { AxiosRequestConfig } from "axios";
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { NotificationIface } from "../constants/app";
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
import EntityIcon from "../components/EntityIcon.vue";
import InfiniteScroll from "../components/InfiniteScroll.vue";
@@ -278,12 +278,14 @@ import OnboardingDialog from "../components/OnboardingDialog.vue";
import ProjectIcon from "../components/ProjectIcon.vue";
import TopMessage from "../components/TopMessage.vue";
import UserNameDialog from "../components/UserNameDialog.vue";
import * as databaseUtil from "../db/databaseUtil";
import { Contact } from "../db/tables/contacts";
import { didInfo, getHeaders, getPlanFromCache } from "../libs/endorserServer";
import { OfferSummaryRecord, PlanData } from "../interfaces/records";
import * as libsUtil from "../libs/util";
import { OnboardPage } from "../libs/util";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
@Component({
components: {
EntityIcon,
@@ -324,13 +326,26 @@ export default class ProjectsView extends Vue {
async mounted() {
try {
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.isRegistered = !!settings.isRegistered;
this.givenName = settings.firstName || "";
this.allContacts = await db.contacts.toArray();
const platformService = PlatformServiceFactory.getInstance();
const queryResult = await platformService.dbQuery(
"SELECT * FROM contacts",
);
this.allContacts = databaseUtil.mapQueryResultToValues(
queryResult,
) as unknown as Contact[];
if (USE_DEXIE_DB) {
await db.open();
this.allContacts = await db.contacts.toArray();
}
this.allMyDids = await libsUtil.retrieveAccountDids();

View File

@@ -72,7 +72,8 @@ import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
import { NotificationIface } from "../constants/app";
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
import * as databaseUtil from "../db/databaseUtil";
import { retrieveSettingsForActiveAccount } from "../db/index";
import {
BVC_MEETUPS_PROJECT_CLAIM_ID,
@@ -117,7 +118,10 @@ export default class QuickActionBvcBeginView extends Vue {
}
async record() {
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
const activeDid = settings.activeDid || "";
const apiServer = settings.apiServer || "";

View File

@@ -144,7 +144,8 @@ import { Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
import { NotificationIface } from "../constants/app";
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
import * as databaseUtil from "../db/databaseUtil";
import {
accountsDBPromise,
db,
@@ -165,6 +166,7 @@ import {
getHeaders,
} from "../libs/endorserServer";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
@Component({
methods: { claimSpecialDescription },
components: {
@@ -191,10 +193,24 @@ export default class QuickActionBvcBeginView extends Vue {
async created() {
this.loadingConfirms = true;
const settings = await retrieveSettingsForActiveAccount();
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount();
}
this.apiServer = settings.apiServer || "";
this.activeDid = settings.activeDid || "";
this.allContacts = await db.contacts.toArray();
const platformService = PlatformServiceFactory.getInstance();
const contactQueryResult = await platformService.dbQuery(
"SELECT * FROM contacts",
);
this.allContacts = databaseUtil.mapQueryResultToValues(
contactQueryResult,
) as unknown as Contact[];
if (USE_DEXIE_DB) {
await db.open();
this.allContacts = await db.contacts.toArray();
}
let currentOrPreviousSat = DateTime.now().setZone("America/Denver");
if (currentOrPreviousSat.weekday < 6) {
@@ -213,10 +229,19 @@ export default class QuickActionBvcBeginView extends Vue {
suppressMilliseconds: true,
}) || "";
const accountsDB = await accountsDBPromise;
await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray();
this.allMyDids = allAccounts.map((acc) => acc.did);
const queryResult = await platformService.dbQuery(
"SELECT did FROM accounts",
);
this.allMyDids =
databaseUtil
.mapQueryResultToValues(queryResult)
?.map((row) => row[0] as string) || [];
if (USE_DEXIE_DB) {
const accountsDB = await accountsDBPromise;
await accountsDB.open();
const allAccounts = await accountsDB.accounts.toArray();
this.allMyDids = allAccounts.map((acc) => acc.did);
}
const headers = await getHeaders(this.activeDid);
try {
const response = await fetch(

Some files were not shown because too many files have changed in this diff Show More