@@ -499,8 +499,10 @@ export default class ImageMethodDialog extends Vue {
*/
async mounted() {
try {
- const settings = await this.$accountSettings();
- this.activeDid = settings.activeDid || "";
+ // Get activeDid from active_identity table (single source of truth)
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const activeIdentity = await (this as any).$getActiveIdentity();
+ this.activeDid = activeIdentity.activeDid || "";
} catch (error) {
logger.error("Error retrieving settings from database:", error);
this.notify.error(
diff --git a/src/components/MembersList.vue b/src/components/MembersList.vue
index ed6b1a32..d556418f 100644
--- a/src/components/MembersList.vue
+++ b/src/components/MembersList.vue
@@ -232,7 +232,12 @@ export default class MembersList extends Vue {
this.notify = createNotifyHelpers(this.$notify);
const settings = await this.$accountSettings();
- this.activeDid = settings.activeDid || "";
+
+ // Get activeDid from active_identity table (single source of truth)
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const activeIdentity = await (this as any).$getActiveIdentity();
+ this.activeDid = activeIdentity.activeDid || "";
+
this.apiServer = settings.apiServer || "";
this.firstName = settings.firstName || "";
await this.fetchMembers();
diff --git a/src/components/OfferDialog.vue b/src/components/OfferDialog.vue
index eeedce82..943a27fc 100644
--- a/src/components/OfferDialog.vue
+++ b/src/components/OfferDialog.vue
@@ -176,7 +176,11 @@ export default class OfferDialog extends Vue {
const settings = await this.$accountSettings();
this.apiServer = settings.apiServer || "";
- this.activeDid = settings.activeDid || "";
+
+ // Get activeDid from active_identity table (single source of truth)
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const activeIdentity = await (this as any).$getActiveIdentity();
+ this.activeDid = activeIdentity.activeDid || "";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
diff --git a/src/components/OnboardingDialog.vue b/src/components/OnboardingDialog.vue
index fe419d55..9c3f8f07 100644
--- a/src/components/OnboardingDialog.vue
+++ b/src/components/OnboardingDialog.vue
@@ -270,7 +270,12 @@ export default class OnboardingDialog extends Vue {
async open(page: OnboardPage) {
this.page = page;
const settings = await this.$accountSettings();
- this.activeDid = settings.activeDid || "";
+
+ // Get activeDid from active_identity table (single source of truth)
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const activeIdentity = await (this as any).$getActiveIdentity();
+ this.activeDid = activeIdentity.activeDid || "";
+
this.isRegistered = !!settings.isRegistered;
const contacts = await this.$getAllContacts();
diff --git a/src/components/PhotoDialog.vue b/src/components/PhotoDialog.vue
index 54511f67..b0a0fe74 100644
--- a/src/components/PhotoDialog.vue
+++ b/src/components/PhotoDialog.vue
@@ -268,7 +268,12 @@ export default class PhotoDialog extends Vue {
// logger.log("PhotoDialog mounted");
try {
const settings = await this.$accountSettings();
- this.activeDid = settings.activeDid || "";
+
+ // Get activeDid from active_identity table (single source of truth)
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const activeIdentity = await (this as any).$getActiveIdentity();
+ this.activeDid = activeIdentity.activeDid || "";
+
this.isRegistered = !!settings.isRegistered;
logger.log("isRegistered:", this.isRegistered);
} catch (error: unknown) {
diff --git a/src/components/TopMessage.vue b/src/components/TopMessage.vue
index a884af74..080aa6dd 100644
--- a/src/components/TopMessage.vue
+++ b/src/components/TopMessage.vue
@@ -49,8 +49,11 @@ export default class TopMessage extends Vue {
logger.debug("[TopMessage] π₯ Loading settings without overrides...");
const settings = await this.$accountSettings();
+ // Get activeDid from new active_identity table (ActiveDid migration)
+ const activeIdentity = await this.$getActiveIdentity();
+
logger.debug("[TopMessage] π Settings loaded:", {
- activeDid: settings.activeDid,
+ activeDid: activeIdentity.activeDid,
apiServer: settings.apiServer,
warnIfTestServer: settings.warnIfTestServer,
warnIfProdServer: settings.warnIfProdServer,
@@ -64,7 +67,7 @@ export default class TopMessage extends Vue {
settings.apiServer &&
settings.apiServer !== AppString.PROD_ENDORSER_API_SERVER
) {
- const didPrefix = settings.activeDid?.slice(11, 15);
+ const didPrefix = activeIdentity.activeDid?.slice(11, 15);
this.message = "You're not using prod, user " + didPrefix;
logger.debug("[TopMessage] β οΈ Test server warning displayed:", {
apiServer: settings.apiServer,
@@ -75,7 +78,7 @@ export default class TopMessage extends Vue {
settings.apiServer &&
settings.apiServer === AppString.PROD_ENDORSER_API_SERVER
) {
- const didPrefix = settings.activeDid?.slice(11, 15);
+ const didPrefix = activeIdentity.activeDid?.slice(11, 15);
this.message = "You are using prod, user " + didPrefix;
logger.debug("[TopMessage] β οΈ Production server warning displayed:", {
apiServer: settings.apiServer,
diff --git a/src/components/UserNameDialog.vue b/src/components/UserNameDialog.vue
index 7a426e7f..1ffd6e6d 100644
--- a/src/components/UserNameDialog.vue
+++ b/src/components/UserNameDialog.vue
@@ -84,7 +84,6 @@ export default class UserNameDialog extends Vue {
*/
async open(aCallback?: (name?: string) => void) {
this.callback = aCallback || this.callback;
- // Load from account-specific settings instead of master settings
const settings = await this.$accountSettings();
this.givenName = settings.firstName || "";
this.visible = true;
@@ -96,9 +95,9 @@ export default class UserNameDialog extends Vue {
*/
async onClickSaveChanges() {
try {
- // Get the current active DID to save to user-specific settings
- const settings = await this.$accountSettings();
- const activeDid = settings.activeDid;
+ // Get activeDid from new active_identity table (ActiveDid migration)
+ const activeIdentity = await this.$getActiveIdentity();
+ const activeDid = activeIdentity.activeDid;
if (activeDid) {
// Save to user-specific settings for the current identity
diff --git a/src/db-sql/migration.ts b/src/db-sql/migration.ts
index 0881dd02..4b541cf8 100644
--- a/src/db-sql/migration.ts
+++ b/src/db-sql/migration.ts
@@ -4,6 +4,13 @@ import {
} from "../services/migrationService";
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
import { arrayBufferToBase64 } from "@/libs/crypto";
+import { logger } from "@/utils/logger";
+
+// Database result interface for SQLite queries
+interface DatabaseResult {
+ values?: unknown[][];
+ [key: string]: unknown;
+}
// Generate a random secret for the secret table
@@ -28,7 +35,7 @@ import { arrayBufferToBase64 } from "@/libs/crypto";
// where they couldn't take action because they couldn't unlock that identity.)
const randomBytes = crypto.getRandomValues(new Uint8Array(32));
-const secretBase64 = arrayBufferToBase64(randomBytes);
+const secretBase64 = arrayBufferToBase64(randomBytes.buffer);
// Each migration can include multiple SQL statements (with semicolons)
const MIGRATIONS = [
@@ -127,9 +134,77 @@ const MIGRATIONS = [
{
name: "003_add_hasBackedUpSeed_to_settings",
sql: `
+ -- Add hasBackedUpSeed field to settings
+ -- This migration assumes master code has been deployed
+ -- The error handling will catch this if column already exists and mark migration as applied
ALTER TABLE settings ADD COLUMN hasBackedUpSeed BOOLEAN DEFAULT FALSE;
`,
},
+ {
+ name: "004_active_identity_management",
+ sql: `
+ -- Migration 004: active_identity_management (CONSOLIDATED)
+ -- Combines original migrations 004, 005, and 006 into single atomic operation
+ -- CRITICAL SECURITY: Uses ON DELETE RESTRICT constraint from the start
+ -- Assumes master code deployed with migration 003 (hasBackedUpSeed)
+
+ -- Enable foreign key constraints for data integrity
+ PRAGMA foreign_keys = ON;
+
+ -- Add UNIQUE constraint to accounts.did for foreign key support
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_accounts_did_unique ON accounts(did);
+
+ -- Create active_identity table with SECURE constraint (ON DELETE RESTRICT)
+ -- This prevents accidental account deletion - critical security feature
+ CREATE TABLE IF NOT EXISTS active_identity (
+ id INTEGER PRIMARY KEY CHECK (id = 1),
+ activeDid TEXT REFERENCES accounts(did) ON DELETE RESTRICT,
+ lastUpdated TEXT NOT NULL DEFAULT (datetime('now'))
+ );
+
+ -- Add performance indexes
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_active_identity_single_record ON active_identity(id);
+
+ -- Seed singleton row (only if not already exists)
+ INSERT INTO active_identity (id, activeDid, lastUpdated)
+ SELECT 1, NULL, datetime('now')
+ WHERE NOT EXISTS (SELECT 1 FROM active_identity WHERE id = 1);
+
+ -- MIGRATE EXISTING DATA: Copy activeDid from settings to active_identity
+ -- This prevents data loss when migration runs on existing databases
+ UPDATE active_identity
+ SET activeDid = (SELECT activeDid FROM settings WHERE id = 1),
+ lastUpdated = datetime('now')
+ WHERE id = 1
+ AND EXISTS (SELECT 1 FROM settings WHERE id = 1 AND activeDid IS NOT NULL AND activeDid != '');
+
+ -- CLEANUP: Remove orphaned settings records and clear legacy activeDid values
+ -- This completes the migration from settings-based to table-based active identity
+ DELETE FROM settings WHERE accountDid IS NULL;
+ UPDATE settings SET activeDid = NULL;
+ `,
+ // Split into individual statements for better error handling
+ statements: [
+ "PRAGMA foreign_keys = ON",
+ "CREATE UNIQUE INDEX IF NOT EXISTS idx_accounts_did_unique ON accounts(did)",
+ `CREATE TABLE IF NOT EXISTS active_identity (
+ id INTEGER PRIMARY KEY CHECK (id = 1),
+ activeDid TEXT REFERENCES accounts(did) ON DELETE RESTRICT,
+ lastUpdated TEXT NOT NULL DEFAULT (datetime('now'))
+ )`,
+ "CREATE UNIQUE INDEX IF NOT EXISTS idx_active_identity_single_record ON active_identity(id)",
+ `INSERT INTO active_identity (id, activeDid, lastUpdated)
+ SELECT 1, NULL, datetime('now')
+ WHERE NOT EXISTS (SELECT 1 FROM active_identity WHERE id = 1)`,
+ `UPDATE active_identity
+ SET activeDid = (SELECT activeDid FROM settings WHERE id = 1),
+ lastUpdated = datetime('now')
+ WHERE id = 1
+ AND EXISTS (SELECT 1 FROM settings WHERE id = 1 AND activeDid IS NOT NULL AND activeDid != '')`,
+ "DELETE FROM settings WHERE accountDid IS NULL",
+ "UPDATE settings SET activeDid = NULL",
+ ],
+ },
];
/**
@@ -141,8 +216,127 @@ export async function runMigrations
(
sqlQuery: (sql: string, params?: unknown[]) => Promise,
extractMigrationNames: (result: T) => Set,
): Promise {
+ logger.debug("[Migration] Starting database migrations");
+
for (const migration of MIGRATIONS) {
+ logger.debug("[Migration] Registering migration:", migration.name);
registerMigration(migration);
}
+
+ logger.debug("[Migration] Running migration service");
await runMigrationsService(sqlExec, sqlQuery, extractMigrationNames);
+ logger.debug("[Migration] Database migrations completed");
+
+ // Bootstrapping: Ensure active account is selected after migrations
+ logger.debug("[Migration] Running bootstrapping hooks");
+ try {
+ // Check if we have accounts but no active selection
+ const accountsResult = await sqlQuery("SELECT COUNT(*) FROM accounts");
+ const accountsCount =
+ accountsResult && (accountsResult as DatabaseResult).values
+ ? ((accountsResult as DatabaseResult).values?.[0]?.[0] as number)
+ : 0;
+
+ // Check if active_identity table exists, and if not, try to recover
+ let activeDid: string | null = null;
+ try {
+ const activeResult = await sqlQuery(
+ "SELECT activeDid FROM active_identity WHERE id = 1",
+ );
+ activeDid =
+ activeResult && (activeResult as DatabaseResult).values
+ ? ((activeResult as DatabaseResult).values?.[0]?.[0] as string)
+ : null;
+ } catch (error) {
+ // Table doesn't exist - this means migration 004 failed but was marked as applied
+ logger.warn(
+ "[Migration] active_identity table missing, attempting recovery",
+ );
+
+ // Check if migration 004 is marked as applied
+ const migrationResult = await sqlQuery(
+ "SELECT name FROM migrations WHERE name = '004_active_identity_management'",
+ );
+ const isMigrationMarked =
+ migrationResult && (migrationResult as DatabaseResult).values
+ ? ((migrationResult as DatabaseResult).values?.length ?? 0) > 0
+ : false;
+
+ if (isMigrationMarked) {
+ logger.warn(
+ "[Migration] Migration 004 marked as applied but table missing - recreating table",
+ );
+
+ // Recreate the active_identity table using the individual statements
+ const statements = [
+ "PRAGMA foreign_keys = ON",
+ "CREATE UNIQUE INDEX IF NOT EXISTS idx_accounts_did_unique ON accounts(did)",
+ `CREATE TABLE IF NOT EXISTS active_identity (
+ id INTEGER PRIMARY KEY CHECK (id = 1),
+ activeDid TEXT REFERENCES accounts(did) ON DELETE RESTRICT,
+ lastUpdated TEXT NOT NULL DEFAULT (datetime('now'))
+ )`,
+ "CREATE UNIQUE INDEX IF NOT EXISTS idx_active_identity_single_record ON active_identity(id)",
+ `INSERT INTO active_identity (id, activeDid, lastUpdated)
+ SELECT 1, NULL, datetime('now')
+ WHERE NOT EXISTS (SELECT 1 FROM active_identity WHERE id = 1)`,
+ `UPDATE active_identity
+ SET activeDid = (SELECT activeDid FROM settings WHERE id = 1),
+ lastUpdated = datetime('now')
+ WHERE id = 1
+ AND EXISTS (SELECT 1 FROM settings WHERE id = 1 AND activeDid IS NOT NULL AND activeDid != '')`,
+ "DELETE FROM settings WHERE accountDid IS NULL",
+ "UPDATE settings SET activeDid = NULL",
+ ];
+
+ for (const statement of statements) {
+ try {
+ await sqlExec(statement);
+ } catch (stmtError) {
+ logger.warn(
+ `[Migration] Recovery statement failed: ${statement}`,
+ stmtError,
+ );
+ }
+ }
+
+ // Try to get activeDid again after recovery
+ try {
+ const activeResult = await sqlQuery(
+ "SELECT activeDid FROM active_identity WHERE id = 1",
+ );
+ activeDid =
+ activeResult && (activeResult as DatabaseResult).values
+ ? ((activeResult as DatabaseResult).values?.[0]?.[0] as string)
+ : null;
+ } catch (recoveryError) {
+ logger.error(
+ "[Migration] Recovery failed - active_identity table still not accessible",
+ recoveryError,
+ );
+ }
+ }
+ }
+
+ if (accountsCount > 0 && (!activeDid || activeDid === "")) {
+ logger.debug("[Migration] Auto-selecting first account as active");
+ const firstAccountResult = await sqlQuery(
+ "SELECT did FROM accounts ORDER BY dateCreated, did LIMIT 1",
+ );
+ const firstAccountDid =
+ firstAccountResult && (firstAccountResult as DatabaseResult).values
+ ? ((firstAccountResult as DatabaseResult).values?.[0]?.[0] as string)
+ : null;
+
+ if (firstAccountDid) {
+ await sqlExec(
+ "UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1",
+ [firstAccountDid],
+ );
+ logger.info(`[Migration] Set active account to: ${firstAccountDid}`);
+ }
+ }
+ } catch (error) {
+ logger.warn("[Migration] Bootstrapping hook failed (non-critical):", error);
+ }
}
diff --git a/src/db/tables/activeIdentity.ts b/src/db/tables/activeIdentity.ts
new file mode 100644
index 00000000..60366bd3
--- /dev/null
+++ b/src/db/tables/activeIdentity.ts
@@ -0,0 +1,14 @@
+/**
+ * ActiveIdentity type describes the active identity selection.
+ * This replaces the activeDid field in the settings table for better
+ * database architecture and data integrity.
+ *
+ * @author Matthew Raymer
+ * @since 2025-08-29
+ */
+
+export interface ActiveIdentity {
+ id: number;
+ activeDid: string;
+ lastUpdated: string;
+}
diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts
index ca8a9e97..02702afc 100644
--- a/src/libs/endorserServer.ts
+++ b/src/libs/endorserServer.ts
@@ -16,7 +16,7 @@
* @module endorserServer
*/
-import { Axios, AxiosRequestConfig } from "axios";
+import { Axios, AxiosRequestConfig, AxiosResponse } from "axios";
import { Buffer } from "buffer";
import { sha256 } from "ethereum-cryptography/sha256";
import { LRUCache } from "lru-cache";
@@ -1131,7 +1131,7 @@ export async function createAndSubmitClaim(
// Enhanced diagnostic logging for claim submission
const requestId = `claim_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
- logger.info("[Claim Submission] π Starting claim submission:", {
+ logger.debug("[Claim Submission] π Starting claim submission:", {
requestId,
apiServer,
requesterDid: issuerDid,
@@ -1157,7 +1157,7 @@ export async function createAndSubmitClaim(
},
});
- logger.info("[Claim Submission] β
Claim submitted successfully:", {
+ logger.debug("[Claim Submission] β
Claim submitted successfully:", {
requestId,
status: response.status,
handleId: response.data?.handleId,
@@ -1754,7 +1754,7 @@ export async function fetchImageRateLimits(
axios: Axios,
issuerDid: string,
imageServer?: string,
-) {
+): Promise {
const server = imageServer || DEFAULT_IMAGE_API_SERVER;
const url = server + "/image-limits";
const headers = await getHeaders(issuerDid);
@@ -1788,7 +1788,7 @@ export async function fetchImageRateLimits(
};
};
- logger.warn("[Image Server] Image rate limits check failed:", {
+ logger.error("[Image Server] Image rate limits check failed:", {
did: issuerDid,
server: server,
errorCode: axiosError.response?.data?.error?.code,
@@ -1796,7 +1796,6 @@ export async function fetchImageRateLimits(
httpStatus: axiosError.response?.status,
timestamp: new Date().toISOString(),
});
-
- throw error;
+ return null;
}
}
diff --git a/src/libs/util.ts b/src/libs/util.ts
index d5f720d7..40d0fd3a 100644
--- a/src/libs/util.ts
+++ b/src/libs/util.ts
@@ -165,18 +165,26 @@ export interface OfferFulfillment {
offerType: string;
}
+interface FulfillmentItem {
+ "@type": string;
+ identifier?: string;
+ [key: string]: unknown;
+}
+
/**
* Extract offer fulfillment information from the fulfills field
* Handles both array and single object cases
*/
-export const extractOfferFulfillment = (fulfills: any): OfferFulfillment | null => {
+export const extractOfferFulfillment = (
+ fulfills: FulfillmentItem | FulfillmentItem[] | null | undefined,
+): OfferFulfillment | null => {
if (!fulfills) {
return null;
}
-
+
// Handle both array and single object cases
let offerFulfill = null;
-
+
if (Array.isArray(fulfills)) {
// Find the Offer in the fulfills array
offerFulfill = fulfills.find((item) => item["@type"] === "Offer");
@@ -184,14 +192,14 @@ export const extractOfferFulfillment = (fulfills: any): OfferFulfillment | null
// fulfills is a single Offer object
offerFulfill = fulfills;
}
-
+
if (offerFulfill) {
return {
- offerHandleId: offerFulfill.identifier,
+ offerHandleId: offerFulfill.identifier || "",
offerType: offerFulfill["@type"],
};
}
-
+
return null;
};
@@ -712,7 +720,8 @@ export async function saveNewIdentity(
];
await platformService.dbExec(sql, params);
- await platformService.updateDefaultSettings({ activeDid: identity.did });
+ // Update active identity in the active_identity table instead of settings
+ await platformService.updateActiveDid(identity.did);
await platformService.insertNewDidIntoSettings(identity.did);
}
@@ -765,7 +774,8 @@ export const registerSaveAndActivatePasskey = async (
): Promise => {
const account = await registerAndSavePasskey(keyName);
const platformService = await getPlatformService();
- await platformService.updateDefaultSettings({ activeDid: account.did });
+ // Update active identity in the active_identity table instead of settings
+ await platformService.updateActiveDid(account.did);
await platformService.updateDidSpecificSettings(account.did, {
isRegistered: false,
});
diff --git a/src/main.capacitor.ts b/src/main.capacitor.ts
index 19cbf4e7..f091770b 100644
--- a/src/main.capacitor.ts
+++ b/src/main.capacitor.ts
@@ -69,18 +69,18 @@ const deepLinkHandler = new DeepLinkHandler(router);
*/
const handleDeepLink = async (data: { url: string }) => {
const { url } = data;
- logger.info(`[Main] π Deeplink received from Capacitor: ${url}`);
+ logger.debug(`[Main] π Deeplink received from Capacitor: ${url}`);
try {
// Wait for router to be ready
- logger.info(`[Main] β³ Waiting for router to be ready...`);
+ logger.debug(`[Main] β³ Waiting for router to be ready...`);
await router.isReady();
- logger.info(`[Main] β
Router is ready, processing deeplink`);
+ logger.debug(`[Main] β
Router is ready, processing deeplink`);
// Process the deeplink
- logger.info(`[Main] π Starting deeplink processing`);
+ logger.debug(`[Main] π Starting deeplink processing`);
await deepLinkHandler.handleDeepLink(url);
- logger.info(`[Main] β
Deeplink processed successfully`);
+ logger.debug(`[Main] β
Deeplink processed successfully`);
} catch (error) {
logger.error(`[Main] β Deeplink processing failed:`, {
url,
@@ -115,25 +115,25 @@ const registerDeepLinkListener = async () => {
);
// Check if Capacitor App plugin is available
- logger.info(`[Main] π Checking Capacitor App plugin availability...`);
+ logger.debug(`[Main] π Checking Capacitor App plugin availability...`);
if (!CapacitorApp) {
throw new Error("Capacitor App plugin not available");
}
logger.info(`[Main] β
Capacitor App plugin is available`);
// Check available methods on CapacitorApp
- logger.info(
+ logger.debug(
`[Main] π Capacitor App plugin methods:`,
Object.getOwnPropertyNames(CapacitorApp),
);
- logger.info(
+ logger.debug(
`[Main] π Capacitor App plugin addListener method:`,
typeof CapacitorApp.addListener,
);
// Wait for router to be ready first
await router.isReady();
- logger.info(
+ logger.debug(
`[Main] β
Router is ready, proceeding with listener registration`,
);
@@ -148,9 +148,6 @@ const registerDeepLinkListener = async () => {
listenerHandle,
);
- // Test the listener registration by checking if it's actually registered
- logger.info(`[Main] π§ͺ Verifying listener registration...`);
-
return listenerHandle;
} catch (error) {
logger.error(`[Main] β Failed to register deeplink listener:`, {
diff --git a/src/main.ts b/src/main.ts
index cc05e386..bbdbd09e 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -24,12 +24,12 @@ logger.info("[Main] π Boot-time environment configuration:", {
// Dynamically import the appropriate main entry point
if (platform === "capacitor") {
- logger.info(`[Main] π± Loading Capacitor-specific entry point`);
+ logger.debug(`[Main] π± Loading Capacitor-specific entry point`);
import("./main.capacitor");
} else if (platform === "electron") {
- logger.info(`[Main] π» Loading Electron-specific entry point`);
+ logger.debug(`[Main] π» Loading Electron-specific entry point`);
import("./main.electron");
} else {
- logger.info(`[Main] π Loading Web-specific entry point`);
+ logger.debug(`[Main] π Loading Web-specific entry point`);
import("./main.web");
}
diff --git a/src/router/index.ts b/src/router/index.ts
index cf450c37..584f7403 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -387,7 +387,7 @@ router.beforeEach(async (to, _from, next) => {
);
}
- logger.info(`[Router] β
Navigation guard passed for: ${to.path}`);
+ logger.debug(`[Router] β
Navigation guard passed for: ${to.path}`);
next();
} catch (error) {
logger.error("[Router] β Identity creation failed in navigation guard:", {
diff --git a/src/services/PlatformService.ts b/src/services/PlatformService.ts
index ede6a5b0..c20e2796 100644
--- a/src/services/PlatformService.ts
+++ b/src/services/PlatformService.ts
@@ -173,6 +173,7 @@ export interface PlatformService {
* @returns Promise that resolves when the update is complete
*/
updateDefaultSettings(settings: Record): Promise;
+ updateActiveDid(did: string): Promise;
/**
* Inserts a new DID into the settings table.
diff --git a/src/services/migrationService.ts b/src/services/migrationService.ts
index 93e769f4..390ad5a5 100644
--- a/src/services/migrationService.ts
+++ b/src/services/migrationService.ts
@@ -73,6 +73,8 @@ interface Migration {
name: string;
/** SQL statement(s) to execute for this migration */
sql: string;
+ /** Optional array of individual SQL statements for better error handling */
+ statements?: string[];
}
/**
@@ -225,6 +227,104 @@ export function registerMigration(migration: Migration): void {
* }
* ```
*/
+/**
+ * Helper function to check if a SQLite result indicates a table exists
+ * @param result - The result from a sqlite_master query
+ * @returns true if the table exists
+ */
+function checkSqliteTableResult(result: unknown): boolean {
+ return (
+ (result as unknown as { values: unknown[][] })?.values?.length > 0 ||
+ (Array.isArray(result) && result.length > 0)
+ );
+}
+
+/**
+ * Helper function to validate that a table exists in the database
+ * @param tableName - Name of the table to check
+ * @param sqlQuery - Function to execute SQL queries
+ * @returns Promise resolving to true if table exists
+ */
+async function validateTableExists(
+ tableName: string,
+ sqlQuery: (sql: string, params?: unknown[]) => Promise,
+): Promise {
+ try {
+ const result = await sqlQuery(
+ `SELECT name FROM sqlite_master WHERE type='table' AND name='${tableName}'`,
+ );
+ return checkSqliteTableResult(result);
+ } catch (error) {
+ logger.error(`β [Validation] Error checking table ${tableName}:`, error);
+ return false;
+ }
+}
+
+/**
+ * Helper function to validate that a column exists in a table
+ * @param tableName - Name of the table
+ * @param columnName - Name of the column to check
+ * @param sqlQuery - Function to execute SQL queries
+ * @returns Promise resolving to true if column exists
+ */
+async function validateColumnExists(
+ tableName: string,
+ columnName: string,
+ sqlQuery: (sql: string, params?: unknown[]) => Promise,
+): Promise {
+ try {
+ await sqlQuery(`SELECT ${columnName} FROM ${tableName} LIMIT 1`);
+ return true;
+ } catch (error) {
+ logger.error(
+ `β [Validation] Error checking column ${columnName} in ${tableName}:`,
+ error,
+ );
+ return false;
+ }
+}
+
+/**
+ * Helper function to validate multiple tables exist
+ * @param tableNames - Array of table names to check
+ * @param sqlQuery - Function to execute SQL queries
+ * @returns Promise resolving to array of validation results
+ */
+async function validateMultipleTables(
+ tableNames: string[],
+ sqlQuery: (sql: string, params?: unknown[]) => Promise,
+): Promise<{ exists: boolean; missing: string[] }> {
+ const missing: string[] = [];
+
+ for (const tableName of tableNames) {
+ const exists = await validateTableExists(tableName, sqlQuery);
+ if (!exists) {
+ missing.push(tableName);
+ }
+ }
+
+ return {
+ exists: missing.length === 0,
+ missing,
+ };
+}
+
+/**
+ * Helper function to add validation error with consistent logging
+ * @param validation - The validation object to update
+ * @param message - Error message to add
+ * @param error - The error object for logging
+ */
+function addValidationError(
+ validation: MigrationValidation,
+ message: string,
+ error: unknown,
+): void {
+ validation.isValid = false;
+ validation.errors.push(message);
+ logger.error(`β [Migration-Validation] ${message}:`, error);
+}
+
async function validateMigrationApplication(
migration: Migration,
sqlQuery: (sql: string, params?: unknown[]) => Promise,
@@ -248,36 +348,82 @@ async function validateMigrationApplication(
"temp",
];
- for (const tableName of tables) {
- try {
- await sqlQuery(
- `SELECT name FROM sqlite_master WHERE type='table' AND name='${tableName}'`,
- );
- // Reduced logging - only log on error
- } catch (error) {
- validation.isValid = false;
- validation.errors.push(`Table ${tableName} missing`);
- logger.error(
- `β [Migration-Validation] Table ${tableName} missing:`,
- error,
- );
- }
+ const tableValidation = await validateMultipleTables(tables, sqlQuery);
+ if (!tableValidation.exists) {
+ validation.isValid = false;
+ validation.errors.push(
+ `Missing tables: ${tableValidation.missing.join(", ")}`,
+ );
+ logger.error(
+ `β [Migration-Validation] Missing tables:`,
+ tableValidation.missing,
+ );
}
- validation.tableExists = validation.errors.length === 0;
+ validation.tableExists = tableValidation.exists;
} else if (migration.name === "002_add_iViewContent_to_contacts") {
// Validate iViewContent column exists in contacts table
- try {
- await sqlQuery(`SELECT iViewContent FROM contacts LIMIT 1`);
+ const columnExists = await validateColumnExists(
+ "contacts",
+ "iViewContent",
+ sqlQuery,
+ );
+ if (!columnExists) {
+ addValidationError(
+ validation,
+ "Column iViewContent missing from contacts table",
+ new Error("Column not found"),
+ );
+ } else {
validation.hasExpectedColumns = true;
- // Reduced logging - only log on error
- } catch (error) {
- validation.isValid = false;
- validation.errors.push(
- `Column iViewContent missing from contacts table`,
+ }
+ } else if (migration.name === "004_active_identity_management") {
+ // Validate active_identity table exists and has correct structure
+ const activeIdentityExists = await validateTableExists(
+ "active_identity",
+ sqlQuery,
+ );
+
+ if (!activeIdentityExists) {
+ addValidationError(
+ validation,
+ "Table active_identity missing",
+ new Error("Table not found"),
);
- logger.error(
- `β [Migration-Validation] Column iViewContent missing:`,
- error,
+ } else {
+ validation.tableExists = true;
+
+ // Check that active_identity has the expected structure
+ const hasExpectedColumns = await validateColumnExists(
+ "active_identity",
+ "id, activeDid, lastUpdated",
+ sqlQuery,
+ );
+
+ if (!hasExpectedColumns) {
+ addValidationError(
+ validation,
+ "active_identity table missing expected columns",
+ new Error("Columns not found"),
+ );
+ } else {
+ validation.hasExpectedColumns = true;
+ }
+ }
+
+ // Check that hasBackedUpSeed column exists in settings table
+ // Note: This validation is included here because migration 004 is consolidated
+ // and includes the functionality from the original migration 003
+ const hasBackedUpSeedExists = await validateColumnExists(
+ "settings",
+ "hasBackedUpSeed",
+ sqlQuery,
+ );
+
+ if (!hasBackedUpSeedExists) {
+ addValidationError(
+ validation,
+ "Column hasBackedUpSeed missing from settings table",
+ new Error("Column not found"),
);
}
}
@@ -343,6 +489,55 @@ async function isSchemaAlreadyPresent(
// Reduced logging - only log on error
return false;
}
+ } else if (migration.name === "003_add_hasBackedUpSeed_to_settings") {
+ // Check if hasBackedUpSeed column exists in settings table
+ try {
+ await sqlQuery(`SELECT hasBackedUpSeed FROM settings LIMIT 1`);
+ return true;
+ } catch (error) {
+ return false;
+ }
+ } else if (migration.name === "004_active_identity_management") {
+ // Check if active_identity table exists and has correct structure
+ try {
+ // Check that active_identity table exists
+ const activeIdentityResult = await sqlQuery(
+ `SELECT name FROM sqlite_master WHERE type='table' AND name='active_identity'`,
+ );
+ const hasActiveIdentityTable =
+ (activeIdentityResult as unknown as { values: unknown[][] })?.values
+ ?.length > 0 ||
+ (Array.isArray(activeIdentityResult) &&
+ activeIdentityResult.length > 0);
+
+ if (!hasActiveIdentityTable) {
+ return false;
+ }
+
+ // Check that active_identity has the expected structure
+ try {
+ await sqlQuery(
+ `SELECT id, activeDid, lastUpdated FROM active_identity LIMIT 1`,
+ );
+
+ // Also check that hasBackedUpSeed column exists in settings
+ // This is included because migration 004 is consolidated
+ try {
+ await sqlQuery(`SELECT hasBackedUpSeed FROM settings LIMIT 1`);
+ return true;
+ } catch (error) {
+ return false;
+ }
+ } catch (error) {
+ return false;
+ }
+ } catch (error) {
+ logger.error(
+ `π [Migration-Schema] Schema check failed for ${migration.name}, assuming not present:`,
+ error,
+ );
+ return false;
+ }
}
// Add schema checks for future migrations here
@@ -412,7 +607,7 @@ export async function runMigrations(
try {
migrationLog("π [Migration] Starting migration process...");
- // Step 1: Create migrations table if it doesn't exist
+ // Create migrations table if it doesn't exist
// Note: We use IF NOT EXISTS here because this is infrastructure, not a business migration
await sqlExec(`
CREATE TABLE IF NOT EXISTS migrations (
@@ -482,7 +677,31 @@ export async function runMigrations(
try {
// Execute the migration SQL
- await sqlExec(migration.sql);
+ migrationLog(`π§ [Migration] Executing SQL for: ${migration.name}`);
+
+ if (migration.statements && migration.statements.length > 0) {
+ // Execute individual statements for better error handling
+ migrationLog(
+ `π§ [Migration] Executing ${migration.statements.length} individual statements`,
+ );
+ for (let i = 0; i < migration.statements.length; i++) {
+ const statement = migration.statements[i];
+ migrationLog(
+ `π§ [Migration] Statement ${i + 1}/${migration.statements.length}: ${statement}`,
+ );
+ const execResult = await sqlExec(statement);
+ migrationLog(
+ `π§ [Migration] Statement ${i + 1} result: ${JSON.stringify(execResult)}`,
+ );
+ }
+ } else {
+ // Execute as single SQL block (legacy behavior)
+ migrationLog(`π§ [Migration] SQL content: ${migration.sql}`);
+ const execResult = await sqlExec(migration.sql);
+ migrationLog(
+ `π§ [Migration] SQL execution result: ${JSON.stringify(execResult)}`,
+ );
+ }
// Validate the migration was applied correctly
const validation = await validateMigrationApplication(
@@ -531,6 +750,8 @@ export async function runMigrations(
`β οΈ [Migration] Schema validation failed for ${migration.name}:`,
validation.errors,
);
+ // Don't mark as applied if validation fails
+ continue;
}
// Mark the migration as applied since the schema change already exists
@@ -558,7 +779,7 @@ export async function runMigrations(
}
}
- // Step 5: Final validation - verify all migrations are properly recorded
+ // Step 6: Final validation - verify all migrations are properly recorded
const finalMigrationsResult = await sqlQuery("SELECT name FROM migrations");
const finalAppliedMigrations = extractMigrationNames(finalMigrationsResult);
diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts
index c1374f25..746f422a 100644
--- a/src/services/platforms/CapacitorPlatformService.ts
+++ b/src/services/platforms/CapacitorPlatformService.ts
@@ -66,13 +66,13 @@ export class CapacitorPlatformService implements PlatformService {
return this.initializationPromise;
}
- // Start initialization
- this.initializationPromise = this._initialize();
try {
+ // Start initialization
+ this.initializationPromise = this._initialize();
await this.initializationPromise;
} catch (error) {
logger.error(
- "[CapacitorPlatformService] Initialize method failed:",
+ "[CapacitorPlatformService] Initialize database method failed:",
error,
);
this.initializationPromise = null; // Reset on failure
@@ -1319,8 +1319,24 @@ export class CapacitorPlatformService implements PlatformService {
await this.dbExec(sql, params);
}
+ async updateActiveDid(did: string): Promise {
+ await this.dbExec(
+ "UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1",
+ [did],
+ );
+ }
+
async insertNewDidIntoSettings(did: string): Promise {
- await this.dbExec("INSERT INTO settings (accountDid) VALUES (?)", [did]);
+ // Import constants dynamically to avoid circular dependencies
+ const { DEFAULT_ENDORSER_API_SERVER, DEFAULT_PARTNER_API_SERVER } =
+ await import("@/constants/app");
+
+ // Use INSERT OR REPLACE to handle case where settings already exist for this DID
+ // This prevents duplicate accountDid entries and ensures data integrity
+ await this.dbExec(
+ "INSERT OR REPLACE INTO settings (accountDid, finishedOnboarding, apiServer, partnerApiServer) VALUES (?, ?, ?, ?)",
+ [did, false, DEFAULT_ENDORSER_API_SERVER, DEFAULT_PARTNER_API_SERVER],
+ );
}
async updateDidSpecificSettings(
diff --git a/src/services/platforms/WebPlatformService.ts b/src/services/platforms/WebPlatformService.ts
index fb15b1b6..dda411d3 100644
--- a/src/services/platforms/WebPlatformService.ts
+++ b/src/services/platforms/WebPlatformService.ts
@@ -674,15 +674,51 @@ export class WebPlatformService implements PlatformService {
async updateDefaultSettings(
settings: Record,
): Promise {
+ // Get current active DID and update that identity's settings
+ const activeIdentity = await this.getActiveIdentity();
+ const activeDid = activeIdentity.activeDid;
+
+ if (!activeDid) {
+ logger.warn(
+ "[WebPlatformService] No active DID found, cannot update default settings",
+ );
+ return;
+ }
+
const keys = Object.keys(settings);
const setClause = keys.map((key) => `${key} = ?`).join(", ");
- const sql = `UPDATE settings SET ${setClause} WHERE id = 1`;
- const params = keys.map((key) => settings[key]);
+ const sql = `UPDATE settings SET ${setClause} WHERE accountDid = ?`;
+ const params = [...keys.map((key) => settings[key]), activeDid];
await this.dbExec(sql, params);
}
+ async updateActiveDid(did: string): Promise {
+ await this.dbExec(
+ "INSERT OR REPLACE INTO active_identity (id, activeDid, lastUpdated) VALUES (1, ?, ?)",
+ [did, new Date().toISOString()],
+ );
+ }
+
+ async getActiveIdentity(): Promise<{ activeDid: string }> {
+ const result = await this.dbQuery(
+ "SELECT activeDid FROM active_identity WHERE id = 1",
+ );
+ return {
+ activeDid: (result?.values?.[0]?.[0] as string) || "",
+ };
+ }
+
async insertNewDidIntoSettings(did: string): Promise {
- await this.dbExec("INSERT INTO settings (accountDid) VALUES (?)", [did]);
+ // Import constants dynamically to avoid circular dependencies
+ const { DEFAULT_ENDORSER_API_SERVER, DEFAULT_PARTNER_API_SERVER } =
+ await import("@/constants/app");
+
+ // Use INSERT OR REPLACE to handle case where settings already exist for this DID
+ // This prevents duplicate accountDid entries and ensures data integrity
+ await this.dbExec(
+ "INSERT OR REPLACE INTO settings (accountDid, finishedOnboarding, apiServer, partnerApiServer) VALUES (?, ?, ?, ?)",
+ [did, false, DEFAULT_ENDORSER_API_SERVER, DEFAULT_PARTNER_API_SERVER],
+ );
}
async updateDidSpecificSettings(
diff --git a/src/utils/PlatformServiceMixin.ts b/src/utils/PlatformServiceMixin.ts
index 010d79ec..c4912afd 100644
--- a/src/utils/PlatformServiceMixin.ts
+++ b/src/utils/PlatformServiceMixin.ts
@@ -45,7 +45,6 @@ import type {
PlatformCapabilities,
} from "@/services/PlatformService";
import {
- MASTER_SETTINGS_KEY,
type Settings,
type SettingsWithJsonStrings,
} from "@/db/tables/settings";
@@ -53,7 +52,11 @@ import { logger } from "@/utils/logger";
import { Contact, ContactMaybeWithJsonStrings } from "@/db/tables/contacts";
import { Account } from "@/db/tables/accounts";
import { Temp } from "@/db/tables/temp";
-import { QueryExecResult, DatabaseExecResult } from "@/interfaces/database";
+import {
+ QueryExecResult,
+ DatabaseExecResult,
+ SqlValue,
+} from "@/interfaces/database";
import {
generateInsertStatement,
generateUpdateStatement,
@@ -210,11 +213,53 @@ export const PlatformServiceMixin = {
logger.debug(
`[PlatformServiceMixin] ActiveDid updated from ${oldDid} to ${newDid}`,
);
+
+ // Write only to active_identity table (single source of truth)
+ try {
+ await this.$dbExec(
+ "UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1",
+ [newDid || ""],
+ );
+ logger.debug(
+ `[PlatformServiceMixin] ActiveDid updated in active_identity table: ${newDid}`,
+ );
+ } catch (error) {
+ logger.error(
+ `[PlatformServiceMixin] Error updating activeDid in active_identity table ${newDid}:`,
+ error,
+ );
+ // Continue with in-memory update even if database write fails
+ }
+
// // Clear caches that might be affected by the change
// this.$clearAllCaches();
}
},
+ /**
+ * Get available account DIDs for user selection
+ * Returns array of DIDs that can be set as active identity
+ */
+ async $getAvailableAccountDids(): Promise {
+ try {
+ const result = await this.$dbQuery(
+ "SELECT did FROM accounts ORDER BY did",
+ );
+
+ if (!result?.values?.length) {
+ return [];
+ }
+
+ return result.values.map((row: SqlValue[]) => row[0] as string);
+ } catch (error) {
+ logger.error(
+ "[PlatformServiceMixin] Error getting available account DIDs:",
+ error,
+ );
+ return [];
+ }
+ },
+
/**
* Map database columns to values with proper type conversion
* Handles boolean conversion from SQLite integers (0/1) to boolean values
@@ -418,7 +463,10 @@ export const PlatformServiceMixin = {
/**
* Enhanced database single row query method with error handling
*/
- async $dbGetOneRow(sql: string, params?: unknown[]) {
+ async $dbGetOneRow(
+ sql: string,
+ params?: unknown[],
+ ): Promise {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return await (this as any).platformService.dbGetOneRow(sql, params);
@@ -444,10 +492,18 @@ export const PlatformServiceMixin = {
fallback: Settings | null = null,
): Promise {
try {
- // Master settings: query by id
+ // Get current active identity
+ const activeIdentity = await this.$getActiveIdentity();
+ const activeDid = activeIdentity.activeDid;
+
+ if (!activeDid) {
+ return fallback;
+ }
+
+ // Get identity-specific settings
const result = await this.$dbQuery(
- "SELECT * FROM settings WHERE id = ?",
- [MASTER_SETTINGS_KEY],
+ "SELECT * FROM settings WHERE accountDid = ?",
+ [activeDid],
);
if (!result?.values?.length) {
@@ -484,7 +540,6 @@ export const PlatformServiceMixin = {
* Handles the common pattern of layered settings
*/
async $getMergedSettings(
- defaultKey: string,
accountDid?: string,
defaultFallback: Settings = {},
): Promise {
@@ -540,7 +595,6 @@ export const PlatformServiceMixin = {
return mergedSettings;
} catch (error) {
logger.error(`[Settings Trace] β Failed to get merged settings:`, {
- defaultKey,
accountDid,
error,
});
@@ -548,6 +602,73 @@ export const PlatformServiceMixin = {
}
},
+ /**
+ * Get active identity from the new active_identity table
+ * This replaces the activeDid field in settings for better architecture
+ */
+ async $getActiveIdentity(): Promise<{ activeDid: string }> {
+ try {
+ const result = await this.$dbQuery(
+ "SELECT activeDid FROM active_identity WHERE id = 1",
+ );
+
+ if (!result?.values?.length) {
+ logger.warn(
+ "[PlatformServiceMixin] Active identity table is empty - this may indicate a migration issue",
+ );
+ return { activeDid: "" };
+ }
+
+ const activeDid = result.values[0][0] as string | null;
+
+ // Handle null activeDid (initial state after migration) - auto-select first account
+ if (activeDid === null) {
+ const firstAccount = await this.$dbQuery(
+ "SELECT did FROM accounts ORDER BY dateCreated, did LIMIT 1",
+ );
+
+ if (firstAccount?.values?.length) {
+ const firstAccountDid = firstAccount.values[0][0] as string;
+ await this.$dbExec(
+ "UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1",
+ [firstAccountDid],
+ );
+ return { activeDid: firstAccountDid };
+ }
+
+ logger.warn(
+ "[PlatformServiceMixin] No accounts available for auto-selection",
+ );
+ return { activeDid: "" };
+ }
+
+ // Validate activeDid exists in accounts
+ const accountExists = await this.$dbQuery(
+ "SELECT did FROM accounts WHERE did = ?",
+ [activeDid],
+ );
+
+ if (accountExists?.values?.length) {
+ return { activeDid };
+ }
+
+ // Clear corrupted activeDid and return empty
+ logger.warn(
+ "[PlatformServiceMixin] Active identity not found in accounts, clearing",
+ );
+ await this.$dbExec(
+ "UPDATE active_identity SET activeDid = NULL, lastUpdated = datetime('now') WHERE id = 1",
+ );
+ return { activeDid: "" };
+ } catch (error) {
+ logger.error(
+ "[PlatformServiceMixin] Error getting active identity:",
+ error,
+ );
+ return { activeDid: "" };
+ }
+ },
+
/**
* Transaction wrapper with automatic rollback on error
*/
@@ -563,6 +684,76 @@ export const PlatformServiceMixin = {
}
},
+ // =================================================
+ // SMART DELETION PATTERN DAL METHODS
+ // =================================================
+
+ /**
+ * Get account DID by ID
+ * Required for smart deletion pattern
+ */
+ async $getAccountDidById(id: number): Promise {
+ const result = await this.$dbQuery(
+ "SELECT did FROM accounts WHERE id = ?",
+ [id],
+ );
+ return result?.values?.[0]?.[0] as string;
+ },
+
+ /**
+ * Get active DID (returns null if none selected)
+ * Required for smart deletion pattern
+ */
+ async $getActiveDid(): Promise {
+ const result = await this.$dbQuery(
+ "SELECT activeDid FROM active_identity WHERE id = 1",
+ );
+ return (result?.values?.[0]?.[0] as string) || null;
+ },
+
+ /**
+ * Set active DID (can be null for no selection)
+ * Required for smart deletion pattern
+ */
+ async $setActiveDid(did: string | null): Promise {
+ await this.$dbExec(
+ "UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1",
+ [did],
+ );
+ },
+
+ /**
+ * Count total accounts
+ * Required for smart deletion pattern
+ */
+ async $countAccounts(): Promise {
+ const result = await this.$dbQuery("SELECT COUNT(*) FROM accounts");
+ return (result?.values?.[0]?.[0] as number) || 0;
+ },
+
+ /**
+ * Deterministic "next" picker for account selection
+ * Required for smart deletion pattern
+ */
+ $pickNextAccountDid(all: string[], current?: string): string {
+ const sorted = [...all].sort();
+ if (!current) return sorted[0];
+ const i = sorted.indexOf(current);
+ return sorted[(i + 1) % sorted.length];
+ },
+
+ /**
+ * Ensure an active account is selected (repair hook)
+ * Required for smart deletion pattern bootstrapping
+ */
+ async $ensureActiveSelected(): Promise {
+ const active = await this.$getActiveDid();
+ const all = await this.$getAllAccountDids();
+ if (active === null && all.length > 0) {
+ await this.$setActiveDid(this.$pickNextAccountDid(all));
+ }
+ },
+
// =================================================
// ULTRA-CONCISE DATABASE METHODS (shortest names)
// =================================================
@@ -601,7 +792,7 @@ export const PlatformServiceMixin = {
async $one(
sql: string,
params: unknown[] = [],
- ): Promise {
+ ): Promise {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return await (this as any).platformService.dbGetOneRow(sql, params);
},
@@ -759,14 +950,14 @@ export const PlatformServiceMixin = {
return defaults;
}
- // FIXED: Remove forced override - respect user preferences
+ // FIXED: Set default apiServer for all platforms, not just Electron
// Only set default if no user preference exists
- if (!settings.apiServer && process.env.VITE_PLATFORM === "electron") {
+ if (!settings.apiServer) {
// Import constants dynamically to get platform-specific values
const { DEFAULT_ENDORSER_API_SERVER } = await import(
"../constants/app"
);
- // Only set if user hasn't specified a preference
+ // Set default for all platforms when apiServer is empty
settings.apiServer = DEFAULT_ENDORSER_API_SERVER;
}
@@ -792,8 +983,9 @@ export const PlatformServiceMixin = {
return defaults;
}
- // Determine which DID to use
- const targetDid = did || defaultSettings.activeDid;
+ // Get DID from active_identity table (single source of truth)
+ const activeIdentity = await this.$getActiveIdentity();
+ const targetDid = did || activeIdentity.activeDid;
// If no target DID, return default settings
if (!targetDid) {
@@ -802,22 +994,29 @@ export const PlatformServiceMixin = {
// Get merged settings using existing method
const mergedSettings = await this.$getMergedSettings(
- MASTER_SETTINGS_KEY,
targetDid,
defaultSettings,
);
- // FIXED: Remove forced override - respect user preferences
+ // Set activeDid from active_identity table (single source of truth)
+ mergedSettings.activeDid = activeIdentity.activeDid;
+ logger.debug(
+ "[PlatformServiceMixin] Using activeDid from active_identity table:",
+ { activeDid: activeIdentity.activeDid },
+ );
+ logger.debug(
+ "[PlatformServiceMixin] $accountSettings() returning activeDid:",
+ { activeDid: mergedSettings.activeDid },
+ );
+
+ // FIXED: Set default apiServer for all platforms, not just Electron
// Only set default if no user preference exists
- if (
- !mergedSettings.apiServer &&
- process.env.VITE_PLATFORM === "electron"
- ) {
+ if (!mergedSettings.apiServer) {
// Import constants dynamically to get platform-specific values
const { DEFAULT_ENDORSER_API_SERVER } = await import(
"../constants/app"
);
- // Only set if user hasn't specified a preference
+ // Set default for all platforms when apiServer is empty
mergedSettings.apiServer = DEFAULT_ENDORSER_API_SERVER;
}
@@ -855,16 +1054,36 @@ export const PlatformServiceMixin = {
async $saveSettings(changes: Partial): Promise {
try {
// Remove fields that shouldn't be updated
- const { accountDid, id, ...safeChanges } = changes;
+ const {
+ accountDid,
+ id,
+ activeDid: activeDidField,
+ ...safeChanges
+ } = changes;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
void accountDid;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
void id;
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ void activeDidField;
+
+ logger.debug(
+ "[PlatformServiceMixin] $saveSettings - Original changes:",
+ changes,
+ );
+ logger.debug(
+ "[PlatformServiceMixin] $saveSettings - Safe changes:",
+ safeChanges,
+ );
if (Object.keys(safeChanges).length === 0) return true;
// Convert settings for database storage (handles searchBoxes conversion)
const convertedChanges = this._convertSettingsForStorage(safeChanges);
+ logger.debug(
+ "[PlatformServiceMixin] $saveSettings - Converted changes:",
+ convertedChanges,
+ );
const setParts: string[] = [];
const params: unknown[] = [];
@@ -876,17 +1095,33 @@ export const PlatformServiceMixin = {
}
});
+ logger.debug(
+ "[PlatformServiceMixin] $saveSettings - Set parts:",
+ setParts,
+ );
+ logger.debug("[PlatformServiceMixin] $saveSettings - Params:", params);
+
if (setParts.length === 0) return true;
- params.push(MASTER_SETTINGS_KEY);
- await this.$dbExec(
- `UPDATE settings SET ${setParts.join(", ")} WHERE id = ?`,
- params,
- );
+ // Get current active DID and update that identity's settings
+ const activeIdentity = await this.$getActiveIdentity();
+ const currentActiveDid = activeIdentity.activeDid;
+
+ if (currentActiveDid) {
+ params.push(currentActiveDid);
+ await this.$dbExec(
+ `UPDATE settings SET ${setParts.join(", ")} WHERE accountDid = ?`,
+ params,
+ );
+ } else {
+ logger.warn(
+ "[PlatformServiceMixin] No active DID found, cannot save settings",
+ );
+ }
// Update activeDid tracking if it changed
- if (changes.activeDid !== undefined) {
- await this.$updateActiveDid(changes.activeDid);
+ if (activeDidField !== undefined) {
+ await this.$updateActiveDid(activeDidField);
}
return true;
@@ -1210,8 +1445,15 @@ export const PlatformServiceMixin = {
*/
async $getAllAccountDids(): Promise {
try {
- const accounts = await this.$query("SELECT did FROM accounts");
- return accounts.map((account) => account.did);
+ const result = await this.$dbQuery(
+ "SELECT did FROM accounts ORDER BY did",
+ );
+
+ if (!result?.values?.length) {
+ return [];
+ }
+
+ return result.values.map((row: SqlValue[]) => row[0] as string);
} catch (error) {
logger.error(
"[PlatformServiceMixin] Error getting all account DIDs:",
@@ -1336,13 +1578,16 @@ export const PlatformServiceMixin = {
fields: string[],
did?: string,
): Promise {
- // Use correct settings table schema
- const whereClause = did ? "WHERE accountDid = ?" : "WHERE id = ?";
- const params = did ? [did] : [MASTER_SETTINGS_KEY];
+ // Use current active DID if no specific DID provided
+ const targetDid = did || (await this.$getActiveIdentity()).activeDid;
+
+ if (!targetDid) {
+ return undefined;
+ }
return await this.$one(
- `SELECT ${fields.join(", ")} FROM settings ${whereClause}`,
- params,
+ `SELECT ${fields.join(", ")} FROM settings WHERE accountDid = ?`,
+ [targetDid],
);
},
@@ -1545,7 +1790,7 @@ export const PlatformServiceMixin = {
const settings = mappedResults[0] as Settings;
- logger.info(`[PlatformServiceMixin] Settings for DID ${did}:`, {
+ logger.debug(`[PlatformServiceMixin] Settings for DID ${did}:`, {
firstName: settings.firstName,
isRegistered: settings.isRegistered,
activeDid: settings.activeDid,
@@ -1572,7 +1817,7 @@ export const PlatformServiceMixin = {
try {
// Get default settings
const defaultSettings = await this.$getMasterSettings({});
- logger.info(
+ logger.debug(
`[PlatformServiceMixin] Default settings:`,
defaultSettings,
);
@@ -1582,12 +1827,11 @@ export const PlatformServiceMixin = {
// Get merged settings
const mergedSettings = await this.$getMergedSettings(
- MASTER_SETTINGS_KEY,
did,
defaultSettings || {},
);
- logger.info(`[PlatformServiceMixin] Merged settings for ${did}:`, {
+ logger.debug(`[PlatformServiceMixin] Merged settings for ${did}:`, {
defaultSettings,
didSettings,
mergedSettings,
@@ -1617,14 +1861,19 @@ export interface IPlatformServiceMixin {
params?: unknown[],
): Promise;
$dbExec(sql: string, params?: unknown[]): Promise;
- $dbGetOneRow(sql: string, params?: unknown[]): Promise;
+ $dbGetOneRow(
+ sql: string,
+ params?: unknown[],
+ ): Promise;
$getMasterSettings(fallback?: Settings | null): Promise;
$getMergedSettings(
defaultKey: string,
accountDid?: string,
defaultFallback?: Settings,
): Promise;
+ $getActiveIdentity(): Promise<{ activeDid: string }>;
$withTransaction(callback: () => Promise): Promise;
+ $getAvailableAccountDids(): Promise;
isCapacitor: boolean;
isWeb: boolean;
isElectron: boolean;
@@ -1718,7 +1967,7 @@ declare module "@vue/runtime-core" {
// Ultra-concise database methods (shortest possible names)
$db(sql: string, params?: unknown[]): Promise;
$exec(sql: string, params?: unknown[]): Promise;
- $one(sql: string, params?: unknown[]): Promise;
+ $one(sql: string, params?: unknown[]): Promise;
// Query + mapping combo methods
$query>(
@@ -1746,7 +1995,9 @@ declare module "@vue/runtime-core" {
did?: string,
defaults?: Settings,
): Promise;
+ $getActiveIdentity(): Promise<{ activeDid: string }>;
$withTransaction(fn: () => Promise): Promise;
+ $getAvailableAccountDids(): Promise;
// Specialized shortcuts - contacts cached, settings fresh
$contacts(): Promise;
diff --git a/src/utils/logger.ts b/src/utils/logger.ts
index 52ae5daa..cf90daac 100644
--- a/src/utils/logger.ts
+++ b/src/utils/logger.ts
@@ -59,10 +59,27 @@ type LogLevel = keyof typeof LOG_LEVELS;
// Parse VITE_LOG_LEVEL environment variable
const getLogLevel = (): LogLevel => {
- const envLogLevel = process.env.VITE_LOG_LEVEL?.toLowerCase();
+ // Try to get VITE_LOG_LEVEL from different sources
+ let envLogLevel: string | undefined;
- if (envLogLevel && envLogLevel in LOG_LEVELS) {
- return envLogLevel as LogLevel;
+ try {
+ // In browser/Vite environment, use import.meta.env
+ if (
+ typeof import.meta !== "undefined" &&
+ import.meta?.env?.VITE_LOG_LEVEL
+ ) {
+ envLogLevel = import.meta.env.VITE_LOG_LEVEL;
+ }
+ // Fallback to process.env for Node.js environments
+ else if (process.env.VITE_LOG_LEVEL) {
+ envLogLevel = process.env.VITE_LOG_LEVEL;
+ }
+ } catch (error) {
+ // Silently handle cases where import.meta is not available
+ }
+
+ if (envLogLevel && envLogLevel.toLowerCase() in LOG_LEVELS) {
+ return envLogLevel.toLowerCase() as LogLevel;
}
// Default log levels based on environment
diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue
index 9f23a955..f4cdaca8 100644
--- a/src/views/AccountViewView.vue
+++ b/src/views/AccountViewView.vue
@@ -27,7 +27,7 @@
need an identifier.
Create An Identifier
@@ -1051,7 +1051,11 @@ export default class AccountViewView extends Vue {
// Then get the account-specific settings
const settings: AccountSettings = await this.$accountSettings();
- this.activeDid = settings.activeDid || "";
+ // Get activeDid from active_identity table (single source of truth)
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const activeIdentity = await (this as any).$getActiveIdentity();
+ this.activeDid = activeIdentity.activeDid || "";
+
this.apiServer = settings.apiServer || "";
this.apiServerInput = settings.apiServer || "";
this.givenName =
@@ -1446,12 +1450,11 @@ export default class AccountViewView extends Vue {
this.DEFAULT_IMAGE_API_SERVER,
);
- if (imageResp.status === 200) {
+ if (imageResp && imageResp.status === 200) {
this.imageLimits = imageResp.data;
} else {
this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.NO_IMAGE_ACCESS;
this.notify.warning(ACCOUNT_VIEW_CONSTANTS.LIMITS.CANNOT_UPLOAD_IMAGES);
- return;
}
const endorserResp = await fetchEndorserRateLimits(
@@ -1465,7 +1468,6 @@ export default class AccountViewView extends Vue {
} else {
this.limitsMessage = ACCOUNT_VIEW_CONSTANTS.LIMITS.NO_LIMITS_FOUND;
this.notify.warning(ACCOUNT_VIEW_CONSTANTS.LIMITS.BAD_SERVER_RESPONSE);
- return;
}
} catch (error) {
this.limitsMessage =
@@ -1482,6 +1484,7 @@ export default class AccountViewView extends Vue {
error: error instanceof Error ? error.message : String(error),
did: did,
apiServer: this.apiServer,
+ imageServer: this.DEFAULT_IMAGE_API_SERVER,
partnerApiServer: this.partnerApiServer,
errorCode: axiosError?.response?.data?.error?.code,
errorMessage: axiosError?.response?.data?.error?.message,
@@ -1996,7 +1999,7 @@ export default class AccountViewView extends Vue {
error: error instanceof Error ? error.message : String(error),
timestamp: new Date().toISOString(),
});
- throw new Error("Failed to load profile");
+ return null;
}
}
diff --git a/src/views/ClaimAddRawView.vue b/src/views/ClaimAddRawView.vue
index ed96a79c..2b4410f3 100644
--- a/src/views/ClaimAddRawView.vue
+++ b/src/views/ClaimAddRawView.vue
@@ -113,7 +113,12 @@ export default class ClaimAddRawView extends Vue {
*/
private async initializeSettings() {
const settings = await this.$accountSettings();
- this.activeDid = settings.activeDid || "";
+
+ // Get activeDid from active_identity table (single source of truth)
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const activeIdentity = await (this as any).$getActiveIdentity();
+ this.activeDid = activeIdentity.activeDid || "";
+
this.apiServer = settings.apiServer || "";
}
diff --git a/src/views/ClaimCertificateView.vue b/src/views/ClaimCertificateView.vue
index 7aed7b52..e2561468 100644
--- a/src/views/ClaimCertificateView.vue
+++ b/src/views/ClaimCertificateView.vue
@@ -40,7 +40,12 @@ export default class ClaimCertificateView extends Vue {
async created() {
this.notify = createNotifyHelpers(this.$notify);
const settings = await this.$accountSettings();
- this.activeDid = settings.activeDid || "";
+
+ // Get activeDid from active_identity table (single source of truth)
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const activeIdentity = await (this as any).$getActiveIdentity();
+ this.activeDid = activeIdentity.activeDid || "";
+
this.apiServer = settings.apiServer || "";
const pathParams = window.location.pathname.substring(
"/claim-cert/".length,
diff --git a/src/views/ClaimReportCertificateView.vue b/src/views/ClaimReportCertificateView.vue
index dbbae98d..a9249003 100644
--- a/src/views/ClaimReportCertificateView.vue
+++ b/src/views/ClaimReportCertificateView.vue
@@ -53,8 +53,13 @@ export default class ClaimReportCertificateView extends Vue {
// Initialize notification helper
this.notify = createNotifyHelpers(this.$notify);
- const settings = await this.$settings();
- this.activeDid = settings.activeDid || "";
+ const settings = await this.$accountSettings();
+
+ // Get activeDid from active_identity table (single source of truth)
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const activeIdentity = await (this as any).$getActiveIdentity();
+ this.activeDid = activeIdentity.activeDid || "";
+
this.apiServer = settings.apiServer || "";
const pathParams = window.location.pathname.substring(
"/claim-cert/".length,
diff --git a/src/views/ClaimView.vue b/src/views/ClaimView.vue
index 77b8ed75..ee095764 100644
--- a/src/views/ClaimView.vue
+++ b/src/views/ClaimView.vue
@@ -736,7 +736,7 @@ export default class ClaimView extends Vue {
*/
extractOfferFulfillment() {
this.detailsForGiveOfferFulfillment = libsUtil.extractOfferFulfillment(
- this.detailsForGive?.fullClaim?.fulfills
+ this.detailsForGive?.fullClaim?.fulfills,
);
}
@@ -767,7 +767,11 @@ export default class ClaimView extends Vue {
const settings = await this.$accountSettings();
- this.activeDid = settings.activeDid || "";
+ // Get activeDid from active_identity table (single source of truth)
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const activeIdentity = await (this as any).$getActiveIdentity();
+ this.activeDid = activeIdentity.activeDid || "";
+
this.apiServer = settings.apiServer || "";
this.allContacts = await this.$contacts();
diff --git a/src/views/ConfirmGiftView.vue b/src/views/ConfirmGiftView.vue
index 4369f04d..974306e8 100644
--- a/src/views/ConfirmGiftView.vue
+++ b/src/views/ConfirmGiftView.vue
@@ -556,7 +556,12 @@ export default class ConfirmGiftView extends Vue {
*/
private async initializeSettings() {
const settings = await this.$accountSettings();
- this.activeDid = settings.activeDid || "";
+
+ // Get activeDid from active_identity table (single source of truth)
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const activeIdentity = await (this as any).$getActiveIdentity();
+ this.activeDid = activeIdentity.activeDid || "";
+
this.apiServer = settings.apiServer || "";
this.allContacts = await this.$getAllContacts();
this.isRegistered = settings.isRegistered || false;
@@ -723,7 +728,7 @@ export default class ConfirmGiftView extends Vue {
*/
private extractOfferFulfillment() {
this.giveDetailsOfferFulfillment = libsUtil.extractOfferFulfillment(
- this.giveDetails?.fullClaim?.fulfills
+ this.giveDetails?.fullClaim?.fulfills,
);
}
diff --git a/src/views/ContactAmountsView.vue b/src/views/ContactAmountsView.vue
index 56ee2061..db8c8bad 100644
--- a/src/views/ContactAmountsView.vue
+++ b/src/views/ContactAmountsView.vue
@@ -224,7 +224,12 @@ export default class ContactAmountssView extends Vue {
this.contact = contact;
const settings = await this.$getMasterSettings();
- this.activeDid = settings?.activeDid || "";
+
+ // Get activeDid from active_identity table (single source of truth)
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const activeIdentity = await (this as any).$getActiveIdentity();
+ this.activeDid = activeIdentity.activeDid || "";
+
this.apiServer = settings?.apiServer || "";
if (this.activeDid && this.contact) {
diff --git a/src/views/ContactGiftingView.vue b/src/views/ContactGiftingView.vue
index 21adf7cf..b4c4f5eb 100644
--- a/src/views/ContactGiftingView.vue
+++ b/src/views/ContactGiftingView.vue
@@ -164,7 +164,11 @@ export default class ContactGiftingView extends Vue {
try {
const settings = await this.$accountSettings();
this.apiServer = settings.apiServer || "";
- this.activeDid = settings.activeDid || "";
+
+ // Get activeDid from active_identity table (single source of truth)
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const activeIdentity = await (this as any).$getActiveIdentity();
+ this.activeDid = activeIdentity.activeDid || "";
this.allContacts = await this.$getAllContacts();
diff --git a/src/views/ContactImportView.vue b/src/views/ContactImportView.vue
index a926d189..2d4eab99 100644
--- a/src/views/ContactImportView.vue
+++ b/src/views/ContactImportView.vue
@@ -340,7 +340,12 @@ export default class ContactImportView extends Vue {
*/
private async initializeSettings() {
const settings = await this.$accountSettings();
- this.activeDid = settings.activeDid || "";
+
+ // Get activeDid from active_identity table (single source of truth)
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const activeIdentity = await (this as any).$getActiveIdentity();
+ this.activeDid = activeIdentity.activeDid || "";
+
this.apiServer = settings.apiServer || "";
}
diff --git a/src/views/ContactQRScanFullView.vue b/src/views/ContactQRScanFullView.vue
index b5dabf3a..15eb5185 100644
--- a/src/views/ContactQRScanFullView.vue
+++ b/src/views/ContactQRScanFullView.vue
@@ -269,7 +269,12 @@ export default class ContactQRScanFull extends Vue {
try {
const settings = await this.$accountSettings();
- this.activeDid = settings.activeDid || "";
+
+ // Get activeDid from active_identity table (single source of truth)
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const activeIdentity = await (this as any).$getActiveIdentity();
+ this.activeDid = activeIdentity.activeDid || "";
+
this.apiServer = settings.apiServer || "";
this.givenName = settings.firstName || "";
this.isRegistered = !!settings.isRegistered;
@@ -393,7 +398,7 @@ export default class ContactQRScanFull extends Vue {
this.isCleaningUp = true;
try {
- logger.info("Cleaning up QR scanner resources");
+ logger.debug("Cleaning up QR scanner resources");
await this.stopScanning();
await QRScannerFactory.cleanup();
} catch (error) {
@@ -427,7 +432,7 @@ export default class ContactQRScanFull extends Vue {
rawValue === this.lastScannedValue &&
now - this.lastScanTime < this.SCAN_DEBOUNCE_MS
) {
- logger.info("Ignoring duplicate scan:", rawValue);
+ logger.debug("Ignoring duplicate scan:", rawValue);
return;
}
@@ -435,7 +440,7 @@ export default class ContactQRScanFull extends Vue {
this.lastScannedValue = rawValue;
this.lastScanTime = now;
- logger.info("Processing QR code scan result:", rawValue);
+ logger.debug("Processing QR code scan result:", rawValue);
let contact: Contact;
if (rawValue.includes(CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI)) {
@@ -448,7 +453,7 @@ export default class ContactQRScanFull extends Vue {
}
// Process JWT and contact info
- logger.info("Decoding JWT payload from QR code");
+ logger.debug("Decoding JWT payload from QR code");
const decodedJwt = await decodeEndorserJwt(jwt);
if (!decodedJwt?.payload?.own) {
logger.warn("Invalid JWT payload - missing 'own' field");
@@ -487,7 +492,7 @@ export default class ContactQRScanFull extends Vue {
}
// Add contact but keep scanning
- logger.info("Adding new contact to database:", {
+ logger.debug("Adding new contact to database:", {
did: contact.did,
name: contact.name,
});
@@ -546,7 +551,7 @@ export default class ContactQRScanFull extends Vue {
*/
async addNewContact(contact: Contact) {
try {
- logger.info("Opening database connection for new contact");
+ logger.debug("Opening database connection for new contact");
// Check if contact already exists
const existingContact = await this.$getContact(contact.did);
@@ -560,7 +565,7 @@ export default class ContactQRScanFull extends Vue {
await this.$insertContact(contact);
if (this.activeDid) {
- logger.info("Setting contact visibility", { did: contact.did });
+ logger.debug("Setting contact visibility", { did: contact.did });
await this.setVisibility(contact, true);
contact.seesMe = true;
}
@@ -607,7 +612,7 @@ export default class ContactQRScanFull extends Vue {
async handleAppPause() {
if (!this.isMounted) return;
- logger.info("App paused, stopping scanner");
+ logger.debug("App paused, stopping scanner");
await this.stopScanning();
}
@@ -617,7 +622,7 @@ export default class ContactQRScanFull extends Vue {
handleAppResume() {
if (!this.isMounted) return;
- logger.info("App resumed, scanner can be restarted by user");
+ logger.debug("App resumed, scanner can be restarted by user");
this.isScanning = false;
}
diff --git a/src/views/ContactQRScanShowView.vue b/src/views/ContactQRScanShowView.vue
index f5f57bee..8afbfecb 100644
--- a/src/views/ContactQRScanShowView.vue
+++ b/src/views/ContactQRScanShowView.vue
@@ -288,7 +288,12 @@ export default class ContactQRScanShow extends Vue {
try {
const settings = await this.$accountSettings();
- this.activeDid = settings.activeDid || "";
+
+ // Get activeDid from active_identity table (single source of truth)
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const activeIdentity = await (this as any).$getActiveIdentity();
+ this.activeDid = activeIdentity.activeDid || "";
+
this.apiServer = settings.apiServer || "";
this.givenName = settings.firstName || "";
this.hideRegisterPromptOnNewContact =
@@ -428,7 +433,7 @@ export default class ContactQRScanShow extends Vue {
this.isCleaningUp = true;
try {
- logger.info("Cleaning up QR scanner resources");
+ logger.debug("Cleaning up QR scanner resources");
await this.stopScanning();
await QRScannerFactory.cleanup();
} catch (error) {
@@ -462,7 +467,7 @@ export default class ContactQRScanShow extends Vue {
rawValue === this.lastScannedValue &&
now - this.lastScanTime < this.SCAN_DEBOUNCE_MS
) {
- logger.info("Ignoring duplicate scan:", rawValue);
+ logger.debug("Ignoring duplicate scan:", rawValue);
return;
}
@@ -470,7 +475,7 @@ export default class ContactQRScanShow extends Vue {
this.lastScannedValue = rawValue;
this.lastScanTime = now;
- logger.info("Processing QR code scan result:", rawValue);
+ logger.debug("Processing QR code scan result:", rawValue);
let contact: Contact;
if (rawValue.includes(CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI)) {
@@ -480,7 +485,7 @@ export default class ContactQRScanShow extends Vue {
this.notify.error(NOTIFY_QR_INVALID_QR_CODE.message);
return;
}
- logger.info("Decoding JWT payload from QR code");
+ logger.debug("Decoding JWT payload from QR code");
const decodedJwt = await decodeEndorserJwt(jwt);
// Process JWT and contact info
@@ -515,7 +520,7 @@ export default class ContactQRScanShow extends Vue {
}
// Add contact but keep scanning
- logger.info("Adding new contact to database:", {
+ logger.debug("Adding new contact to database:", {
did: contact.did,
name: contact.name,
});
@@ -549,7 +554,7 @@ export default class ContactQRScanShow extends Vue {
}
async register(contact: Contact) {
- logger.info("Submitting contact registration", {
+ logger.debug("Submitting contact registration", {
did: contact.did,
name: contact.name,
});
@@ -565,7 +570,7 @@ export default class ContactQRScanShow extends Vue {
if (regResult.success) {
contact.registered = true;
await this.$updateContact(contact.did, { registered: true });
- logger.info("Contact registration successful", { did: contact.did });
+ logger.debug("Contact registration successful", { did: contact.did });
this.notify.success(
createQRRegistrationSuccessMessage(contact.name || ""),
@@ -691,20 +696,20 @@ export default class ContactQRScanShow extends Vue {
async handleAppPause() {
if (!this.isMounted) return;
- logger.info("App paused, stopping scanner");
+ logger.debug("App paused, stopping scanner");
await this.stopScanning();
}
handleAppResume() {
if (!this.isMounted) return;
- logger.info("App resumed, scanner can be restarted by user");
+ logger.debug("App resumed, scanner can be restarted by user");
this.isScanning = false;
}
async addNewContact(contact: Contact) {
try {
- logger.info("Opening database connection for new contact");
+ logger.debug("Opening database connection for new contact");
// Check if contact already exists
const existingContact = await this.$getContact(contact.did);
@@ -731,7 +736,7 @@ export default class ContactQRScanShow extends Vue {
await this.$insertContact(contact);
if (this.activeDid) {
- logger.info("Setting contact visibility", { did: contact.did });
+ logger.debug("Setting contact visibility", { did: contact.did });
await this.setVisibility(contact, true);
contact.seesMe = true;
}
diff --git a/src/views/ContactsView.vue b/src/views/ContactsView.vue
index 2ed7611f..23d4fbe9 100644
--- a/src/views/ContactsView.vue
+++ b/src/views/ContactsView.vue
@@ -174,7 +174,7 @@ import { logger } from "../utils/logger";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { isDatabaseError } from "@/interfaces/common";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
-import { APP_SERVER } from "@/constants/app";
+import { APP_SERVER, DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
import { QRNavigationService } from "@/services/QRNavigationService";
import {
NOTIFY_CONTACT_NO_INFO,
@@ -294,10 +294,19 @@ export default class ContactsView extends Vue {
this.notify = createNotifyHelpers(this.$notify);
const settings = await this.$accountSettings();
- this.activeDid = settings.activeDid || "";
- this.apiServer = settings.apiServer || "";
+ // Get activeDid from active_identity table (single source of truth)
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const activeIdentity = await (this as any).$getActiveIdentity();
+ this.activeDid = activeIdentity.activeDid || "";
+ this.apiServer = settings.apiServer || DEFAULT_ENDORSER_API_SERVER;
this.isRegistered = !!settings.isRegistered;
+ logger.debug("[ContactsView] Created with settings:", {
+ activeDid: this.activeDid,
+ apiServer: this.apiServer,
+ isRegistered: this.isRegistered,
+ });
+
// if these detect a query parameter, they can and then redirect to this URL without a query parameter
// to avoid problems when they reload or they go forward & back and it tries to reprocess
await this.processContactJwt();
@@ -346,15 +355,34 @@ export default class ContactsView extends Vue {
// this happens when a platform (eg iOS) doesn't include anything after the "=" in a shared link.
this.notify.error(NOTIFY_BLANK_INVITE.message, TIMEOUTS.VERY_LONG);
} else if (importedInviteJwt) {
+ logger.debug("[ContactsView] Processing invite JWT, current activeDid:", {
+ activeDid: this.activeDid,
+ });
+
+ // Re-fetch settings after ensuring active_identity is populated
+ const updatedSettings = await this.$accountSettings();
+ this.activeDid = updatedSettings.activeDid || "";
+ this.apiServer = updatedSettings.apiServer || DEFAULT_ENDORSER_API_SERVER;
+
// Identity creation should be handled by router guard, but keep as fallback for invite processing
if (!this.activeDid) {
logger.info(
"[ContactsView] No active DID found, creating identity as fallback for invite processing",
);
this.activeDid = await generateSaveAndActivateIdentity();
+ logger.info("[ContactsView] Created new identity:", {
+ activeDid: this.activeDid,
+ });
}
// send invite directly to server, with auth for this user
const headers = await getHeaders(this.activeDid);
+ logger.debug("[ContactsView] Making API request to claim invite:", {
+ apiServer: this.apiServer,
+ activeDid: this.activeDid,
+ hasApiServer: !!this.apiServer,
+ apiServerLength: this.apiServer?.length || 0,
+ fullUrl: this.apiServer + "/api/v2/claim",
+ });
try {
const response = await this.axios.post(
this.apiServer + "/api/v2/claim",
@@ -376,6 +404,9 @@ export default class ContactsView extends Vue {
const payload: JWTPayload =
decodeEndorserJwt(importedInviteJwt).payload;
const registration = payload as VerifiableCredential;
+ logger.debug(
+ "[ContactsView] Opening ContactNameDialog for invite processing",
+ );
(this.$refs.contactNameDialog as ContactNameDialog).open(
"Who Invited You?",
"",
@@ -414,17 +445,28 @@ export default class ContactsView extends Vue {
this.$logAndConsole(fullError, true);
let message = "Got an error sending the invite.";
if (
+ error &&
+ typeof error === "object" &&
+ "response" in error &&
error.response &&
+ typeof error.response === "object" &&
+ "data" in error.response &&
error.response.data &&
- error.response.data.error
+ typeof error.response.data === "object" &&
+ "error" in error.response.data
) {
- if (error.response.data.error.message) {
- message = error.response.data.error.message;
+ const responseData = error.response.data as { error: unknown };
+ if (
+ responseData.error &&
+ typeof responseData.error === "object" &&
+ "message" in responseData.error
+ ) {
+ message = (responseData.error as { message: string }).message;
} else {
- message = error.response.data.error;
+ message = String(responseData.error);
}
- } else if (error.message) {
- message = error.message;
+ } else if (error && typeof error === "object" && "message" in error) {
+ message = (error as { message: string }).message;
}
this.notify.error(message, TIMEOUTS.MODAL);
}
diff --git a/src/views/DIDView.vue b/src/views/DIDView.vue
index 4a793d0e..93de83c3 100644
--- a/src/views/DIDView.vue
+++ b/src/views/DIDView.vue
@@ -376,7 +376,12 @@ export default class DIDView extends Vue {
*/
private async initializeSettings() {
const settings = await this.$accountSettings();
- this.activeDid = settings.activeDid || "";
+
+ // Get activeDid from active_identity table (single source of truth)
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const activeIdentity = await (this as any).$getActiveIdentity();
+ this.activeDid = activeIdentity.activeDid || "";
+
this.apiServer = settings.apiServer || "";
}
diff --git a/src/views/DatabaseMigration.vue b/src/views/DatabaseMigration.vue
index 4002584f..30a39266 100644
--- a/src/views/DatabaseMigration.vue
+++ b/src/views/DatabaseMigration.vue
@@ -1261,7 +1261,7 @@ export default class DatabaseMigration extends Vue {
this.comparison.differences.settings.added.length +
this.comparison.differences.accounts.added.length;
this.successMessage = `Comparison completed successfully. Found ${totalItems} items to migrate.`;
- logger.info(
+ logger.debug(
"[DatabaseMigration] Database comparison completed successfully",
);
} catch (error) {
@@ -1313,7 +1313,7 @@ export default class DatabaseMigration extends Vue {
this.successMessage += ` ${result.warnings.length} warnings.`;
this.warning += result.warnings.join(", ");
}
- logger.info(
+ logger.debug(
"[DatabaseMigration] Settings migration completed successfully",
result,
);
@@ -1356,7 +1356,7 @@ export default class DatabaseMigration extends Vue {
this.successMessage += ` ${result.warnings.length} warnings.`;
this.warning += result.warnings.join(", ");
}
- logger.info(
+ logger.debug(
"[DatabaseMigration] Account migration completed successfully",
result,
);
@@ -1406,7 +1406,7 @@ export default class DatabaseMigration extends Vue {
URL.revokeObjectURL(url);
this.successMessage = "Comparison data exported successfully";
- logger.info("[DatabaseMigration] Comparison data exported successfully");
+ logger.debug("[DatabaseMigration] Comparison data exported successfully");
} catch (error) {
this.error = `Failed to export comparison data: ${error}`;
logger.error("[DatabaseMigration] Export failed:", error);
diff --git a/src/views/DiscoverView.vue b/src/views/DiscoverView.vue
index 2a8879e0..778f9eb2 100644
--- a/src/views/DiscoverView.vue
+++ b/src/views/DiscoverView.vue
@@ -415,7 +415,11 @@ export default class DiscoverView extends Vue {
const searchPeople = !!this.$route.query["searchPeople"];
const settings = await this.$accountSettings();
- this.activeDid = (settings.activeDid as string) || "";
+
+ // Get activeDid from new active_identity table (ActiveDid migration)
+ const activeIdentity = await this.$getActiveIdentity();
+ this.activeDid = activeIdentity.activeDid || "";
+
this.apiServer = (settings.apiServer as string) || "";
this.partnerApiServer =
(settings.partnerApiServer as string) || this.partnerApiServer;
diff --git a/src/views/GiftedDetailsView.vue b/src/views/GiftedDetailsView.vue
index 812c0b02..990a8b20 100644
--- a/src/views/GiftedDetailsView.vue
+++ b/src/views/GiftedDetailsView.vue
@@ -442,7 +442,11 @@ export default class GiftedDetails extends Vue {
const settings = await this.$accountSettings();
this.apiServer = settings.apiServer || "";
- this.activeDid = settings.activeDid || "";
+
+ // Get activeDid from active_identity table (single source of truth)
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const activeIdentity = await (this as any).$getActiveIdentity();
+ this.activeDid = activeIdentity.activeDid || "";
if (
(this.giverDid && !this.giverName) ||
diff --git a/src/views/HelpView.vue b/src/views/HelpView.vue
index 52332bd1..b09aedc7 100644
--- a/src/views/HelpView.vue
+++ b/src/views/HelpView.vue
@@ -694,7 +694,10 @@ export default class HelpView extends Vue {
try {
const settings = await this.$accountSettings();
- if (settings.activeDid) {
+ // Get activeDid from new active_identity table (ActiveDid migration)
+ const activeIdentity = await this.$getActiveIdentity();
+
+ if (activeIdentity.activeDid) {
await this.$updateSettings({
...settings,
finishedOnboarding: false,
@@ -702,7 +705,7 @@ export default class HelpView extends Vue {
this.$log(
"[HelpView] Onboarding reset successfully for DID: " +
- settings.activeDid,
+ activeIdentity.activeDid,
);
}
diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue
index 45a5d5bb..9374d079 100644
--- a/src/views/HomeView.vue
+++ b/src/views/HomeView.vue
@@ -238,7 +238,7 @@ Raymer * @version 1.0.0 */