Merge branch 'master' into new-activity-mark-read

This commit is contained in:
Jose Olarte III
2025-09-19 15:14:42 +08:00
90 changed files with 4025 additions and 413 deletions

View File

@@ -386,7 +386,7 @@ export default class App extends Vue {
let allGoingOff = false;
try {
const settings: Settings = await this.$settings();
const settings: Settings = await this.$accountSettings();
const notifyingNewActivity = !!settings?.notifyingNewActivityTime;
const notifyingReminder = !!settings?.notifyingReminderTime;

View File

@@ -220,9 +220,18 @@ export default class GiftedDialog extends Vue {
this.stepType = "giver";
try {
const settings = await this.$settings();
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 || "";
logger.debug("[GiftedDialog] Settings received:", {
activeDid: this.activeDid,
apiServer: this.apiServer,
});
this.allContacts = await this.$contacts();

View File

@@ -132,7 +132,7 @@
v-if="shouldMirrorVideo"
class="absolute top-2 left-2 bg-black/50 text-white px-2 py-1 rounded text-xs"
>
<font-awesome icon="mirror" class="w-[1em] mr-1" />
<font-awesome icon="circle-user" class="w-[1em] mr-1" />
Mirrored
</div>
<div :class="cameraControlsClasses">
@@ -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(

View File

@@ -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();

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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

View File

@@ -4,6 +4,7 @@ import {
} from "../services/migrationService";
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
import { arrayBufferToBase64 } from "@/libs/crypto";
import { logger } from "@/utils/logger";
// Generate a random secret for the secret table
@@ -28,7 +29,53 @@ 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);
// Single source of truth for migration 004 SQL
const MIG_004_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
-- Use guarded operations to prevent accidental data loss
DELETE FROM settings WHERE accountDid IS NULL AND id != 1;
UPDATE settings SET activeDid = NULL WHERE id = 1 AND EXISTS (
SELECT 1 FROM active_identity WHERE id = 1 AND activeDid IS NOT NULL
);
`;
// Each migration can include multiple SQL statements (with semicolons)
const MIGRATIONS = [
@@ -127,11 +174,42 @@ 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: MIG_004_SQL,
},
];
/**
* Extract single value from database query result
* Works with different database service result formats
*/
function extractSingleValue<T>(result: T): string | number | null {
if (!result) return null;
// Handle AbsurdSQL format: QueryExecResult[]
if (Array.isArray(result) && result.length > 0 && result[0]?.values) {
const values = result[0].values;
return values.length > 0 ? values[0][0] : null;
}
// Handle Capacitor SQLite format: { values: unknown[][] }
if (typeof result === "object" && result !== null && "values" in result) {
const values = (result as { values: unknown[][] }).values;
return values && values.length > 0
? (values[0][0] as string | number)
: null;
}
return null;
}
/**
* @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"
@@ -141,8 +219,73 @@ export async function runMigrations<T>(
sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
extractMigrationNames: (result: T) => Set<string>,
): Promise<void> {
// Only log migration start in development
const isDevelopment = process.env.VITE_PLATFORM === "development";
if (isDevelopment) {
logger.debug("[Migration] Starting database migrations");
}
for (const migration of MIGRATIONS) {
if (isDevelopment) {
logger.debug("[Migration] Registering migration:", migration.name);
}
registerMigration(migration);
}
if (isDevelopment) {
logger.debug("[Migration] Running migration service");
}
await runMigrationsService(sqlExec, sqlQuery, extractMigrationNames);
if (isDevelopment) {
logger.debug("[Migration] Database migrations completed");
}
// Bootstrapping: Ensure active account is selected after migrations
if (isDevelopment) {
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 = (extractSingleValue(accountsResult) 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 = (extractSingleValue(activeResult) as string) || null;
} catch (error) {
// Table doesn't exist - migration 004 may not have run yet
if (isDevelopment) {
logger.debug(
"[Migration] active_identity table not found - migration may not have run",
);
}
activeDid = null;
}
if (accountsCount > 0 && (!activeDid || activeDid === "")) {
if (isDevelopment) {
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 =
(extractSingleValue(firstAccountResult) 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);
}
}

View File

@@ -567,6 +567,8 @@ export async function debugSettingsData(did?: string): Promise<void> {
* - Web SQLite (wa-sqlite/absurd-sql): Auto-parses JSON strings to objects
* - Capacitor SQLite: Returns raw strings that need manual parsing
*
* Maybe consolidate with PlatformServiceMixin._parseJsonField
*
* @param value The value to parse (could be string or already parsed object)
* @param defaultValue Default value if parsing fails
* @returns Parsed object or default value

View File

@@ -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;
}

View File

@@ -9,6 +9,8 @@ export type Contact = {
// When adding a property:
// - Consider whether it should be added when exporting & sharing contacts, eg. DataExportSection
// - If it's a boolean, it should be converted from a 0/1 integer in PlatformServiceMixin._mapColumnsToValues
// - If it's a JSON string, it should be converted to an object/array in PlatformServiceMixin._mapColumnsToValues
//
did: string;
contactMethods?: Array<ContactMethod>;

View File

@@ -14,6 +14,12 @@ export type BoundingBox = {
* New entries that are boolean should also be added to PlatformServiceMixin._mapColumnsToValues
*/
export type Settings = {
//
// When adding a property:
// - If it's a boolean, it should be converted from a 0/1 integer in PlatformServiceMixin._mapColumnsToValues
// - If it's a JSON string, it should be converted to an object/array in PlatformServiceMixin._mapColumnsToValues
//
// default entry is keyed with MASTER_SETTINGS_KEY; other entries are linked to an account with account ID
id?: string | number; // this is erased for all those entries that are keyed with accountDid

View File

@@ -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";
@@ -315,7 +315,7 @@ export function didInfoForContact(
return { displayName: "You", known: true };
} else if (contact) {
return {
displayName: contact.name || "Contact With No Name",
displayName: contact.name || "Contact Without a Name",
known: true,
profileImageUrl: contact.profileImageUrl,
};
@@ -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<AxiosResponse | null> {
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;
}
}

View File

@@ -165,9 +165,10 @@ export interface OfferFulfillment {
offerType: string;
}
interface FulfillmentObject {
interface FulfillmentItem {
"@type": string;
identifier?: string;
[key: string]: unknown;
}
/**
@@ -175,7 +176,7 @@ interface FulfillmentObject {
* Handles both array and single object cases
*/
export const extractOfferFulfillment = (
fulfills: FulfillmentObject | FulfillmentObject[] | null | undefined,
fulfills: FulfillmentItem | FulfillmentItem[] | null | undefined,
): OfferFulfillment | null => {
if (!fulfills) {
return null;
@@ -194,7 +195,7 @@ export const extractOfferFulfillment = (
if (offerFulfill) {
return {
offerHandleId: offerFulfill.identifier,
offerHandleId: offerFulfill.identifier || "",
offerType: offerFulfill["@type"],
};
}
@@ -719,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);
}
@@ -772,7 +774,8 @@ export const registerSaveAndActivatePasskey = async (
): Promise<Account> => {
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,
});

View File

@@ -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:`, {

View File

@@ -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");
}

View File

@@ -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:", {

View File

@@ -155,6 +155,16 @@ export interface PlatformService {
*/
dbGetOneRow(sql: string, params?: unknown[]): Promise<unknown[] | undefined>;
/**
* Not recommended except for debugging.
* Return the raw result of a SQL query.
*
* @param sql - The SQL query to execute
* @param params - The parameters to pass to the query
* @returns Promise resolving to the raw query result, or undefined if no results
*/
dbRawQuery(sql: string, params?: unknown[]): Promise<unknown | undefined>;
// Database utility methods
/**
* Generates an INSERT SQL statement for a given model and table.
@@ -173,6 +183,7 @@ export interface PlatformService {
* @returns Promise that resolves when the update is complete
*/
updateDefaultSettings(settings: Record<string, unknown>): Promise<void>;
updateActiveDid(did: string): Promise<void>;
/**
* Inserts a new DID into the settings table.

View File

@@ -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<T>(
tableName: string,
sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
): Promise<boolean> {
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<T>(
tableName: string,
columnName: string,
sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
): Promise<boolean> {
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<T>(
tableNames: string[],
sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
): 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<T>(
migration: Migration,
sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
@@ -248,36 +348,82 @@ async function validateMigrationApplication<T>(
"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,
);
}
}
validation.tableExists = validation.errors.length === 0;
} 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`);
validation.hasExpectedColumns = true;
// Reduced logging - only log on error
} catch (error) {
const tableValidation = await validateMultipleTables(tables, sqlQuery);
if (!tableValidation.exists) {
validation.isValid = false;
validation.errors.push(
`Column iViewContent missing from contacts table`,
`Missing tables: ${tableValidation.missing.join(", ")}`,
);
logger.error(
`❌ [Migration-Validation] Column iViewContent missing:`,
error,
`❌ [Migration-Validation] Missing tables:`,
tableValidation.missing,
);
}
validation.tableExists = tableValidation.exists;
} else if (migration.name === "002_add_iViewContent_to_contacts") {
// Validate iViewContent column exists in contacts table
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;
}
} 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"),
);
} 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<T>(
// 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
@@ -404,15 +599,10 @@ export async function runMigrations<T>(
sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
extractMigrationNames: (result: T) => Set<string>,
): Promise<void> {
const isDevelopment = process.env.VITE_PLATFORM === "development";
// Use debug level for routine migration messages in development
const migrationLog = isDevelopment ? logger.debug : logger.log;
try {
migrationLog("📋 [Migration] Starting migration process...");
logger.debug("📋 [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 (
@@ -436,7 +626,8 @@ export async function runMigrations<T>(
return;
}
migrationLog(
// Only log migration counts in development
logger.debug(
`📊 [Migration] Found ${migrations.length} total migrations, ${appliedMigrations.size} already applied`,
);
@@ -448,22 +639,22 @@ export async function runMigrations<T>(
// Check 1: Is it recorded as applied in migrations table?
const isRecordedAsApplied = appliedMigrations.has(migration.name);
// Check 2: Does the schema already exist in the database?
const isSchemaPresent = await isSchemaAlreadyPresent(migration, sqlQuery);
// Skip if already recorded as applied
// Skip if already recorded as applied (name-only check)
if (isRecordedAsApplied) {
skippedCount++;
continue;
}
// Check 2: Does the schema already exist in the database?
const isSchemaPresent = await isSchemaAlreadyPresent(migration, sqlQuery);
// Handle case where schema exists but isn't recorded
if (isSchemaPresent) {
try {
await sqlExec("INSERT INTO migrations (name) VALUES (?)", [
migration.name,
]);
migrationLog(
logger.debug(
`✅ [Migration] Marked existing schema as applied: ${migration.name}`,
);
skippedCount++;
@@ -478,11 +669,20 @@ export async function runMigrations<T>(
}
// Apply the migration
migrationLog(`🔄 [Migration] Applying migration: ${migration.name}`);
logger.debug(`🔄 [Migration] Applying migration: ${migration.name}`);
try {
// Execute the migration SQL
await sqlExec(migration.sql);
// Execute the migration SQL as single atomic operation
logger.debug(`🔧 [Migration] Executing SQL for: ${migration.name}`);
logger.debug(`🔧 [Migration] SQL content: ${migration.sql}`);
// Execute the migration SQL directly - it should be atomic
// The SQL itself should handle any necessary transactions
const execResult = await sqlExec(migration.sql);
logger.debug(
`🔧 [Migration] SQL execution result: ${JSON.stringify(execResult)}`,
);
// Validate the migration was applied correctly
const validation = await validateMigrationApplication(
@@ -501,11 +701,33 @@ export async function runMigrations<T>(
migration.name,
]);
migrationLog(`🎉 [Migration] Successfully applied: ${migration.name}`);
logger.debug(`🎉 [Migration] Successfully applied: ${migration.name}`);
appliedCount++;
} catch (error) {
logger.error(`❌ [Migration] Error applying ${migration.name}:`, error);
// Provide explicit rollback instructions for migration failures
logger.error(
`🔄 [Migration] ROLLBACK INSTRUCTIONS for ${migration.name}:`,
);
logger.error(` 1. Stop the application immediately`);
logger.error(
` 2. Restore database from pre-migration backup/snapshot`,
);
logger.error(
` 3. Remove migration entry: DELETE FROM migrations WHERE name = '${migration.name}'`,
);
logger.error(
` 4. Verify database state matches pre-migration condition`,
);
logger.error(` 5. Restart application and investigate root cause`);
logger.error(
` FAILURE CAUSE: ${error instanceof Error ? error.message : String(error)}`,
);
logger.error(
` REQUIRED OPERATOR ACTION: Manual database restoration required`,
);
// Handle specific cases where the migration might be partially applied
const errorMessage = String(error).toLowerCase();
@@ -517,7 +739,7 @@ export async function runMigrations<T>(
(errorMessage.includes("table") &&
errorMessage.includes("already exists"))
) {
migrationLog(
logger.debug(
`⚠️ [Migration] ${migration.name} appears already applied (${errorMessage}). Validating and marking as complete.`,
);
@@ -531,6 +753,8 @@ export async function runMigrations<T>(
`⚠️ [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
@@ -538,7 +762,7 @@ export async function runMigrations<T>(
await sqlExec("INSERT INTO migrations (name) VALUES (?)", [
migration.name,
]);
migrationLog(`✅ [Migration] Marked as applied: ${migration.name}`);
logger.debug(`✅ [Migration] Marked as applied: ${migration.name}`);
appliedCount++;
} catch (insertError) {
// If we can't insert the migration record, log it but don't fail
@@ -558,7 +782,7 @@ export async function runMigrations<T>(
}
}
// 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);
@@ -574,8 +798,8 @@ export async function runMigrations<T>(
);
}
// Always show completion message
logger.log(
// Only show completion message in development
logger.debug(
`🎉 [Migration] Migration process complete! Summary: ${appliedCount} applied, ${skippedCount} skipped`,
);
} catch (error) {

View File

@@ -24,7 +24,7 @@ import {
import { logger } from "../../utils/logger";
interface QueuedOperation {
type: "run" | "query";
type: "run" | "query" | "rawQuery";
sql: string;
params: unknown[];
resolve: (value: unknown) => void;
@@ -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
@@ -159,6 +159,14 @@ export class CapacitorPlatformService implements PlatformService {
};
break;
}
case "rawQuery": {
const queryResult = await this.db.query(
operation.sql,
operation.params,
);
result = queryResult;
break;
}
}
operation.resolve(result);
} catch (error) {
@@ -500,9 +508,24 @@ export class CapacitorPlatformService implements PlatformService {
// This is essential for proper parameter binding and SQL injection prevention
await this.db!.run(sql, params);
} else {
// Use execute method for non-parameterized queries
// This is more efficient for simple DDL statements
await this.db!.execute(sql);
// For multi-statement SQL (like migrations), use executeSet method
// This handles multiple statements properly
if (
sql.includes(";") &&
sql.split(";").filter((s) => s.trim()).length > 1
) {
// Multi-statement SQL - use executeSet for proper handling
const statements = sql.split(";").filter((s) => s.trim());
await this.db!.executeSet(
statements.map((stmt) => ({
statement: stmt.trim(),
values: [], // Empty values array for non-parameterized statements
})),
);
} else {
// Single statement - use execute method
await this.db!.execute(sql);
}
}
};
@@ -1270,6 +1293,14 @@ export class CapacitorPlatformService implements PlatformService {
return undefined;
}
/**
* @see PlatformService.dbRawQuery
*/
async dbRawQuery(sql: string, params?: unknown[]): Promise<unknown> {
await this.waitForInitialization();
return this.queueOperation("rawQuery", sql, params || []);
}
/**
* Checks if running on Capacitor platform.
* @returns true, as this is the Capacitor implementation
@@ -1319,8 +1350,24 @@ export class CapacitorPlatformService implements PlatformService {
await this.dbExec(sql, params);
}
async updateActiveDid(did: string): Promise<void> {
await this.dbExec(
"UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1",
[did],
);
}
async insertNewDidIntoSettings(did: string): Promise<void> {
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(

View File

@@ -636,6 +636,17 @@ export class WebPlatformService implements PlatformService {
} as GetOneRowRequest);
}
/**
* @see PlatformService.dbRawQuery
*/
async dbRawQuery(
sql: string,
params?: unknown[],
): Promise<unknown | undefined> {
// This class doesn't post-process the result, so we can just use it.
return this.dbQuery(sql, params);
}
/**
* Rotates the camera between front and back cameras.
* @returns Promise that resolves when the camera is rotated
@@ -674,15 +685,51 @@ export class WebPlatformService implements PlatformService {
async updateDefaultSettings(
settings: Record<string, unknown>,
): Promise<void> {
// 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<void> {
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<void> {
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(

View File

@@ -66,7 +66,7 @@ export async function testServerRegisterUser() {
// Make a payload for the claim
const vcPayload = {
sub: "RegisterAction",
sub: identity0.did,
vc: {
"@context": ["https://www.w3.org/2018/credentials/v1"],
type: ["VerifiableCredential"],

View File

@@ -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<string[]> {
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
@@ -230,16 +275,22 @@ export const PlatformServiceMixin = {
// Convert SQLite integer booleans to JavaScript booleans
if (
// settings
column === "isRegistered" ||
column === "finishedOnboarding" ||
column === "filterFeedByVisible" ||
column === "filterFeedByNearby" ||
column === "hasBackedUpSeed" ||
column === "hideRegisterPromptOnNewContact" ||
column === "showContactGivesInline" ||
column === "showGeneralAdvanced" ||
column === "showShortcutBvc" ||
column === "warnIfProdServer" ||
column === "warnIfTestServer"
column === "warnIfTestServer" ||
// contacts
column === "iViewContent" ||
column === "registered" ||
column === "seesMe"
) {
if (value === 1) {
value = true;
@@ -249,13 +300,9 @@ export const PlatformServiceMixin = {
// Keep null values as null
}
// Handle JSON fields like contactMethods
if (column === "contactMethods" && typeof value === "string") {
try {
value = JSON.parse(value);
} catch {
value = [];
}
// Convert SQLite JSON strings to objects/arrays
if (column === "contactMethods" || column === "searchBoxes") {
value = this._parseJsonField(value, []);
}
obj[column] = value;
@@ -265,10 +312,13 @@ export const PlatformServiceMixin = {
},
/**
* Self-contained implementation of parseJsonField
* Safely parses JSON strings with fallback to default value
* Safely parses JSON strings with fallback to default value.
* Handles different SQLite implementations:
* - Web SQLite (wa-sqlite/absurd-sql): Auto-parses JSON strings to objects
* - Capacitor SQLite: Returns raw strings that need manual parsing
*
* Consolidate this with src/libs/util.ts parseJsonField
* See also src/db/databaseUtil.ts parseJsonField
* and maybe consolidate
*/
_parseJsonField<T>(value: unknown, defaultValue: T): T {
if (typeof value === "string") {
@@ -418,7 +468,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<SqlValue[] | undefined> {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return await (this as any).platformService.dbGetOneRow(sql, params);
@@ -436,6 +489,27 @@ export const PlatformServiceMixin = {
}
},
/**
* Database raw query method with error handling
*/
async $dbRawQuery(sql: string, params?: unknown[]) {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return await (this as any).platformService.dbRawQuery(sql, params);
} catch (error) {
logger.error(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
`[${(this as any).$options.name}] Database raw query failed:`,
{
sql,
params,
error,
},
);
throw error;
}
},
/**
* Utility method for retrieving master settings
* Common pattern used across many components
@@ -444,10 +518,18 @@ export const PlatformServiceMixin = {
fallback: Settings | null = null,
): Promise<Settings | null> {
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 +566,6 @@ export const PlatformServiceMixin = {
* Handles the common pattern of layered settings
*/
async $getMergedSettings(
defaultKey: string,
accountDid?: string,
defaultFallback: Settings = {},
): Promise<Settings> {
@@ -540,7 +621,6 @@ export const PlatformServiceMixin = {
return mergedSettings;
} catch (error) {
logger.error(`[Settings Trace] ❌ Failed to get merged settings:`, {
defaultKey,
accountDid,
error,
});
@@ -548,6 +628,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 +710,76 @@ export const PlatformServiceMixin = {
}
},
// =================================================
// SMART DELETION PATTERN DAL METHODS
// =================================================
/**
* Get account DID by ID
* Required for smart deletion pattern
*/
async $getAccountDidById(id: number): Promise<string> {
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<string | null> {
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<void> {
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<number> {
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<void> {
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 +818,7 @@ export const PlatformServiceMixin = {
async $one(
sql: string,
params: unknown[] = [],
): Promise<unknown[] | undefined> {
): Promise<SqlValue[] | undefined> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return await (this as any).platformService.dbGetOneRow(sql, params);
},
@@ -759,14 +976,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 +1009,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 +1020,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 +1080,36 @@ export const PlatformServiceMixin = {
async $saveSettings(changes: Partial<Settings>): Promise<boolean> {
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 +1121,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 +1471,15 @@ export const PlatformServiceMixin = {
*/
async $getAllAccountDids(): Promise<string[]> {
try {
const accounts = await this.$query<Account>("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 +1604,16 @@ export const PlatformServiceMixin = {
fields: string[],
did?: string,
): Promise<unknown[] | undefined> {
// 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 +1816,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 +1843,7 @@ export const PlatformServiceMixin = {
try {
// Get default settings
const defaultSettings = await this.$getMasterSettings({});
logger.info(
logger.debug(
`[PlatformServiceMixin] Default settings:`,
defaultSettings,
);
@@ -1582,12 +1853,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 +1887,20 @@ export interface IPlatformServiceMixin {
params?: unknown[],
): Promise<QueryExecResult | undefined>;
$dbExec(sql: string, params?: unknown[]): Promise<DatabaseExecResult>;
$dbGetOneRow(sql: string, params?: unknown[]): Promise<unknown[] | undefined>;
$dbGetOneRow(
sql: string,
params?: unknown[],
): Promise<SqlValue[] | undefined>;
$dbRawQuery(sql: string, params?: unknown[]): Promise<unknown | undefined>;
$getMasterSettings(fallback?: Settings | null): Promise<Settings | null>;
$getMergedSettings(
defaultKey: string,
accountDid?: string,
defaultFallback?: Settings,
): Promise<Settings>;
$getActiveIdentity(): Promise<{ activeDid: string }>;
$withTransaction<T>(callback: () => Promise<T>): Promise<T>;
$getAvailableAccountDids(): Promise<string[]>;
isCapacitor: boolean;
isWeb: boolean;
isElectron: boolean;
@@ -1718,7 +1994,7 @@ declare module "@vue/runtime-core" {
// Ultra-concise database methods (shortest possible names)
$db(sql: string, params?: unknown[]): Promise<QueryExecResult | undefined>;
$exec(sql: string, params?: unknown[]): Promise<DatabaseExecResult>;
$one(sql: string, params?: unknown[]): Promise<unknown[] | undefined>;
$one(sql: string, params?: unknown[]): Promise<SqlValue[] | undefined>;
// Query + mapping combo methods
$query<T = Record<string, unknown>>(
@@ -1740,13 +2016,16 @@ declare module "@vue/runtime-core" {
sql: string,
params?: unknown[],
): Promise<unknown[] | undefined>;
$dbRawQuery(sql: string, params?: unknown[]): Promise<unknown | undefined>;
$getMasterSettings(defaults?: Settings | null): Promise<Settings | null>;
$getMergedSettings(
key: string,
did?: string,
defaults?: Settings,
): Promise<Settings>;
$getActiveIdentity(): Promise<{ activeDid: string }>;
$withTransaction<T>(fn: () => Promise<T>): Promise<T>;
$getAvailableAccountDids(): Promise<string[]>;
// Specialized shortcuts - contacts cached, settings fresh
$contacts(): Promise<Contact[]>;

View File

@@ -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

View File

@@ -27,7 +27,7 @@
need an identifier.
</p>
<router-link
:to="{ name: 'start' }"
:to="{ name: 'new-identifier' }"
class="inline-block 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-4 py-2 rounded-md"
>
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;
}
}

View File

@@ -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 || "";
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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();

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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 || "";
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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 || "";
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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) ||

View File

@@ -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,
);
}

View File

@@ -238,7 +238,7 @@ Raymer * @version 1.0.0 */
<script lang="ts">
import { UAParser } from "ua-parser-js";
import { Component, Vue } from "vue-facing-decorator";
import { Component, Vue, Watch } from "vue-facing-decorator";
import { Router } from "vue-router";
//import App from "../App.vue";
@@ -283,6 +283,7 @@ import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { NOTIFY_CONTACT_LOADING_ISSUE } from "@/constants/notifications";
import * as Package from "../../package.json";
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
import { errorStringForLog } from "../libs/endorserServer";
// consolidate this with GiveActionClaim in src/interfaces/claims.ts
interface Claim {
@@ -399,6 +400,44 @@ export default class HomeView extends Vue {
newOffersToUserProjectsHitLimit: boolean = false;
numNewOffersToUser: number = 0; // number of new offers-to-user
numNewOffersToUserProjects: number = 0; // number of new offers-to-user's-projects
/**
* CRITICAL VUE REACTIVITY BUG WORKAROUND
*
* This watcher is required for the component to render correctly.
* Without it, the newDirectOffersActivityNumber element fails to render
* even when numNewOffersToUser has the correct value.
*
* This appears to be a Vue reactivity issue where property changes
* don't trigger proper template updates.
*
* DO NOT REMOVE until the underlying Vue reactivity issue is resolved.
*
* See: doc/activeDid-migration-plan.md for details
*/
@Watch("numNewOffersToUser")
onNumNewOffersToUserChange(newValue: number, oldValue: number) {
logger.debug("[HomeView] numNewOffersToUser changed", {
oldValue,
newValue,
willRender: !!newValue,
vIfCondition: `v-if="numNewOffersToUser"`,
elementTestId: "newDirectOffersActivityNumber",
shouldShowElement: newValue > 0,
timestamp: new Date().toISOString(),
});
}
// get shouldShowNewOffersToUser() {
// const shouldShow = !!this.numNewOffersToUser;
// logger.debug("[HomeView] shouldShowNewOffersToUser computed", {
// numNewOffersToUser: this.numNewOffersToUser,
// shouldShow,
// timestamp: new Date().toISOString()
// });
// return shouldShow;
// }
searchBoxes: Array<{
name: string;
bbox: BoundingBox;
@@ -432,13 +471,44 @@ export default class HomeView extends Vue {
*/
async mounted() {
try {
logger.debug("[HomeView] mounted() - component lifecycle started", {
timestamp: new Date().toISOString(),
componentName: "HomeView",
});
await this.initializeIdentity();
// Settings already loaded in initializeIdentity()
await this.loadContacts();
// Contacts already loaded in initializeIdentity()
// Registration check already handled in initializeIdentity()
await this.loadFeedData();
logger.debug("[HomeView] mounted() - about to call loadNewOffers()", {
timestamp: new Date().toISOString(),
activeDid: this.activeDid,
hasActiveDid: !!this.activeDid,
});
await this.loadNewOffers();
logger.debug("[HomeView] mounted() - loadNewOffers() completed", {
timestamp: new Date().toISOString(),
numNewOffersToUser: this.numNewOffersToUser,
numNewOffersToUserProjects: this.numNewOffersToUserProjects,
shouldShowElement:
this.numNewOffersToUser + this.numNewOffersToUserProjects > 0,
});
await this.checkOnboarding();
logger.debug("[HomeView] mounted() - component lifecycle completed", {
timestamp: new Date().toISOString(),
finalState: {
numNewOffersToUser: this.numNewOffersToUser,
numNewOffersToUserProjects: this.numNewOffersToUserProjects,
shouldShowElement:
this.numNewOffersToUser + this.numNewOffersToUserProjects > 0,
},
});
} catch (err: unknown) {
this.handleError(err);
}
@@ -515,11 +585,22 @@ export default class HomeView extends Vue {
// **CRITICAL**: Ensure correct API server for platform
await this.ensureCorrectApiServer();
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 || "";
logger.debug("[HomeView] ActiveDid migration - using new API", {
activeDid: this.activeDid,
source: "active_identity table",
hasActiveDid: !!this.activeDid,
activeIdentityResult: activeIdentity,
isRegistered: this.isRegistered,
timestamp: new Date().toISOString(),
});
// Load contacts with graceful fallback
try {
this.loadContacts();
await this.loadContacts();
} catch (error) {
this.$logAndConsole(
`[HomeView] Failed to retrieve contacts: ${error}`,
@@ -654,24 +735,103 @@ export default class HomeView extends Vue {
* @requires Active DID
*/
private async loadNewOffers() {
if (this.activeDid) {
const offersToUserData = await getNewOffersToUser(
this.axios,
this.apiServer,
this.activeDid,
this.lastAckedOfferToUserJwtId,
);
this.numNewOffersToUser = offersToUserData.data.length;
this.newOffersToUserHitLimit = offersToUserData.hitLimit;
logger.debug("[HomeView] loadNewOffers() called with activeDid:", {
activeDid: this.activeDid,
hasActiveDid: !!this.activeDid,
length: this.activeDid?.length || 0,
});
const offersToUserProjects = await getNewOffersToUserProjects(
this.axios,
this.apiServer,
this.activeDid,
this.lastAckedOfferToUserProjectsJwtId,
if (this.activeDid) {
logger.debug(
"[HomeView] loadNewOffers() - activeDid found, calling API",
{
activeDid: this.activeDid,
apiServer: this.apiServer,
isRegistered: this.isRegistered,
lastAckedOfferToUserJwtId: this.lastAckedOfferToUserJwtId,
},
);
this.numNewOffersToUserProjects = offersToUserProjects.data.length;
this.newOffersToUserProjectsHitLimit = offersToUserProjects.hitLimit;
try {
const offersToUserData = await getNewOffersToUser(
this.axios,
this.apiServer,
this.activeDid,
this.lastAckedOfferToUserJwtId,
);
logger.debug(
"[HomeView] loadNewOffers() - getNewOffersToUser successful",
{
activeDid: this.activeDid,
dataLength: offersToUserData.data.length,
hitLimit: offersToUserData.hitLimit,
},
);
this.numNewOffersToUser = offersToUserData.data.length;
this.newOffersToUserHitLimit = offersToUserData.hitLimit;
logger.debug("[HomeView] loadNewOffers() - updated component state", {
activeDid: this.activeDid,
numNewOffersToUser: this.numNewOffersToUser,
newOffersToUserHitLimit: this.newOffersToUserHitLimit,
willRender: !!this.numNewOffersToUser,
timestamp: new Date().toISOString(),
});
const offersToUserProjects = await getNewOffersToUserProjects(
this.axios,
this.apiServer,
this.activeDid,
this.lastAckedOfferToUserProjectsJwtId,
);
logger.debug(
"[HomeView] loadNewOffers() - getNewOffersToUserProjects successful",
{
activeDid: this.activeDid,
dataLength: offersToUserProjects.data.length,
hitLimit: offersToUserProjects.hitLimit,
},
);
this.numNewOffersToUserProjects = offersToUserProjects.data.length;
this.newOffersToUserProjectsHitLimit = offersToUserProjects.hitLimit;
logger.debug("[HomeView] loadNewOffers() - all API calls completed", {
numNewOffersToUser: this.numNewOffersToUser,
numNewOffersToUserProjects: this.numNewOffersToUserProjects,
shouldRenderElement: !!this.numNewOffersToUser,
elementTestId: "newDirectOffersActivityNumber",
timestamp: new Date().toISOString(),
});
// Additional logging for template rendering debugging
logger.debug("[HomeView] loadNewOffers() - template rendering check", {
numNewOffersToUser: this.numNewOffersToUser,
numNewOffersToUserProjects: this.numNewOffersToUserProjects,
totalNewOffers:
this.numNewOffersToUser + this.numNewOffersToUserProjects,
shouldShowElement:
this.numNewOffersToUser + this.numNewOffersToUserProjects > 0,
vIfCondition: `v-if="numNewOffersToUser + numNewOffersToUserProjects"`,
elementWillRender:
this.numNewOffersToUser + this.numNewOffersToUserProjects > 0,
timestamp: new Date().toISOString(),
});
} catch (error) {
logger.error("[HomeView] loadNewOffers() - API call failed", {
activeDid: this.activeDid,
apiServer: this.apiServer,
isRegistered: this.isRegistered,
error: errorStringForLog(error),
errorMessage: error instanceof Error ? error.message : String(error),
});
}
} else {
logger.warn("[HomeView] loadNewOffers() - no activeDid available", {
activeDid: this.activeDid,
timestamp: new Date().toISOString(),
});
}
}

View File

@@ -200,7 +200,12 @@ export default class IdentitySwitcherView extends Vue {
async created() {
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.apiServerInput = settings.apiServer || "";
@@ -222,8 +227,8 @@ export default class IdentitySwitcherView extends Vue {
}
async switchAccount(did?: string) {
// Save the new active DID to master settings
await this.$saveSettings({ activeDid: did });
// Update the active DID in the active_identity table
await this.$updateActiveDid(did);
// Check if we need to load user-specific settings for the new DID
if (did) {
@@ -267,15 +272,48 @@ export default class IdentitySwitcherView extends Vue {
this.notify.confirm(
NOTIFY_DELETE_IDENTITY_CONFIRM.text,
async () => {
await this.$exec(`DELETE FROM accounts WHERE id = ?`, [id]);
this.otherIdentities = this.otherIdentities.filter(
(ident) => ident.id !== id,
);
await this.smartDeleteAccount(id);
},
-1,
);
}
/**
* Smart deletion with atomic transaction and last account protection
* Follows the Active Pointer + Smart Deletion Pattern
*/
async smartDeleteAccount(id: string) {
await this.$withTransaction(async () => {
const total = await this.$countAccounts();
if (total <= 1) {
this.notify.warning(
"Cannot delete the last account. Keep at least one.",
);
throw new Error("blocked:last-item");
}
const accountDid = await this.$getAccountDidById(parseInt(id));
const activeDid = await this.$getActiveDid();
if (activeDid === accountDid) {
const allDids = await this.$getAllAccountDids();
const nextDid = this.$pickNextAccountDid(
allDids.filter((d) => d !== accountDid),
accountDid,
);
await this.$setActiveDid(nextDid);
this.notify.success(`Switched active to ${nextDid} before deletion.`);
}
await this.$exec("DELETE FROM accounts WHERE id = ?", [id]);
});
// Update UI
this.otherIdentities = this.otherIdentities.filter(
(ident) => ident.id !== id,
);
}
notifyCannotDelete() {
this.notify.warning(
NOTIFY_CANNOT_DELETE_ACTIVE_IDENTITY.message,

View File

@@ -224,13 +224,14 @@ export default class ImportAccountView extends Vue {
);
// Check what was actually imported
const settings = await this.$accountSettings();
// Check account-specific settings
if (settings?.activeDid) {
// Get activeDid from new active_identity table (ActiveDid migration)
const activeIdentity = await this.$getActiveIdentity();
if (activeIdentity.activeDid) {
try {
await this.$query("SELECT * FROM settings WHERE accountDid = ?", [
settings.activeDid,
activeIdentity.activeDid,
]);
} catch (error) {
// Log error but don't interrupt import flow

View File

@@ -120,7 +120,12 @@ export default class InviteOneAcceptView extends Vue {
// Load or generate identity
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 || "";
// Identity creation should be handled by router guard, but keep as fallback for deep links

View File

@@ -283,7 +283,12 @@ export default class InviteOneView extends Vue {
try {
// Use PlatformServiceMixin for account settings
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.isRegistered = !!settings.isRegistered;

View File

@@ -205,7 +205,12 @@ export default class NewActivityView 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.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId || "";
this.lastAckedOfferToUserProjectsJwtId =
settings.lastAckedOfferToUserProjectsJwtId || "";

View File

@@ -110,9 +110,9 @@ export default class NewEditAccountView extends Vue {
* @async
*/
async onClickSaveChanges() {
// 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

View File

@@ -378,7 +378,12 @@ export default class NewEditProjectView extends Vue {
this.numAccounts = await retrieveAccountCount();
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.showGeneralAdvanced = !!settings.showGeneralAdvanced;

View File

@@ -433,7 +433,12 @@ export default class OfferDetailsView extends Vue {
private async loadAccountSettings() {
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.showGeneralAdvanced = settings.showGeneralAdvanced ?? false;
}

View File

@@ -780,7 +780,12 @@ export default class ProjectViewView 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.allContacts = await this.$getAllContacts();
this.isRegistered = !!settings.isRegistered;

View File

@@ -391,7 +391,12 @@ export default class ProjectsView extends Vue {
*/
private async initializeUserSettings() {
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.isRegistered = !!settings.isRegistered;
this.givenName = settings.firstName || "";

View File

@@ -150,7 +150,11 @@ export default class QuickActionBvcBeginView extends Vue {
// Get account settings using PlatformServiceMixin
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 || "";
const apiServer = settings.apiServer || "";
if (!activeDid || !apiServer) {

View File

@@ -234,9 +234,13 @@ export default class QuickActionBvcEndView extends Vue {
// Initialize notification helper
this.notify = createNotifyHelpers(this.$notify);
const settings = await this.$settings();
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.$contacts();

View File

@@ -124,7 +124,12 @@ export default class RecentOffersToUserView 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.lastAckedOfferToUserProjectsJwtId =
settings.lastAckedOfferToUserProjectsJwtId || "";

View File

@@ -116,7 +116,12 @@ export default class RecentOffersToUserView 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.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId || "";
this.allContacts = await this.$getAllContacts();

View File

@@ -206,7 +206,7 @@ export default class SearchAreaView extends Vue {
this.searchBox = settings.searchBoxes?.[0] || null;
this.resetLatLong();
logger.info("[SearchAreaView] Component mounted", {
logger.debug("[SearchAreaView] Component mounted", {
hasStoredSearchBox: !!this.searchBox,
searchBoxName: this.searchBox?.name,
coordinates: this.searchBox?.bbox,
@@ -317,7 +317,7 @@ export default class SearchAreaView extends Vue {
this.searchBox = newSearchBox;
this.isChoosingSearchBox = false;
logger.info("[SearchAreaView] Search box stored successfully", {
logger.debug("[SearchAreaView] Search box stored successfully", {
searchBox: newSearchBox,
coordinates: newSearchBox.bbox,
});
@@ -360,7 +360,7 @@ export default class SearchAreaView extends Vue {
this.isChoosingSearchBox = false;
this.isNewMarkerSet = false;
logger.info("[SearchAreaView] Search box deleted successfully");
logger.debug("[SearchAreaView] Search box deleted successfully");
// Enhanced notification system with proper timeout
this.notify?.success(NOTIFY_SEARCH_AREA_DELETED.text, TIMEOUTS.STANDARD);

View File

@@ -206,8 +206,10 @@ export default class SeedBackupView extends Vue {
async created() {
try {
let activeDid = "";
const settings = await this.$accountSettings();
activeDid = settings.activeDid || "";
// Get activeDid from new active_identity table (ActiveDid migration)
const activeIdentity = await this.$getActiveIdentity();
activeDid = activeIdentity.activeDid || "";
this.numAccounts = await retrieveAccountCount();
this.activeAccount = await retrieveFullyDecryptedAccount(activeDid);
@@ -238,9 +240,10 @@ export default class SeedBackupView extends Vue {
// Update the account setting to track that user has backed up their seed
try {
const settings = await this.$accountSettings();
if (settings.activeDid) {
await this.$saveUserSettings(settings.activeDid, {
// Get activeDid from new active_identity table (ActiveDid migration)
const activeIdentity = await this.$getActiveIdentity();
if (activeIdentity.activeDid) {
await this.$saveUserSettings(activeIdentity.activeDid, {
hasBackedUpSeed: true,
});
}

View File

@@ -76,7 +76,7 @@ export default class ShareMyContactInfoView extends Vue {
isLoading = false;
async mounted() {
const settings = await this.$settings();
const settings = await this.$accountSettings();
const activeDid = settings?.activeDid;
if (!activeDid) {
this.$router.push({ name: "home" });
@@ -91,8 +91,8 @@ export default class ShareMyContactInfoView extends Vue {
this.isLoading = true;
try {
const settings = await this.$settings();
const account = await this.retrieveAccount(settings);
const settings = await this.$accountSettings();
const account = await this.retrieveAccount();
if (!account) {
this.showAccountError();
@@ -114,10 +114,11 @@ export default class ShareMyContactInfoView extends Vue {
/**
* Retrieve the fully decrypted account for the active DID
*/
private async retrieveAccount(
settings: Settings,
): Promise<Account | undefined> {
const activeDid = settings.activeDid || "";
private async retrieveAccount(): Promise<Account | undefined> {
// Get activeDid from new active_identity table (ActiveDid migration)
const activeIdentity = await this.$getActiveIdentity();
const activeDid = activeIdentity.activeDid || "";
if (!activeDid) {
return undefined;
}

View File

@@ -175,8 +175,10 @@ export default class SharedPhotoView extends Vue {
this.notify = createNotifyHelpers(this.$notify);
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;
const temp = await this.$getTemp(SHARED_PHOTO_BASE64_KEY);
const imageB64 = temp?.blobB64 as string;

View File

@@ -68,10 +68,18 @@
placeholder="Enter your SQL query here..."
></textarea>
</div>
<div class="mt-4">
<div class="mt-4 flex items-center gap-4">
<button :class="primaryButtonClasses" @click="executeSql">
Execute
</button>
<label class="flex items-center gap-2">
<input
v-model="returnRawResults"
type="checkbox"
class="rounded border-gray-300"
/>
<span class="text-sm">Return Raw Results (only raw for queries)</span>
</label>
</div>
<div v-if="sqlResult" class="mt-4">
<h3 class="text-lg font-semibold mb-2">Result:</h3>
@@ -401,6 +409,7 @@ export default class Help extends Vue {
// for SQL operations
sqlQuery = "";
sqlResult: unknown = null;
returnRawResults = false;
cryptoLib = cryptoLib;
@@ -625,12 +634,12 @@ export default class Help extends Vue {
* Uses PlatformServiceMixin for database access
*/
async mounted() {
logger.info(
logger.debug(
"[TestView] 🚀 Component mounting - starting URL flow tracking",
);
// Boot-time logging for initial configuration
logger.info("[TestView] 🌍 Boot-time configuration detected:", {
logger.debug("[TestView] 🌍 Boot-time configuration detected:", {
platform: process.env.VITE_PLATFORM,
defaultEndorserApiServer: process.env.VITE_DEFAULT_ENDORSER_API_SERVER,
defaultPartnerApiServer: process.env.VITE_DEFAULT_PARTNER_API_SERVER,
@@ -643,8 +652,11 @@ export default class Help extends Vue {
logger.info("[TestView] 📥 Loading account settings...");
const settings = await this.$accountSettings();
// Get activeDid from new active_identity table (ActiveDid migration)
const activeIdentity = await this.$getActiveIdentity();
logger.info("[TestView] 📊 Settings loaded:", {
activeDid: settings.activeDid,
activeDid: activeIdentity.activeDid,
apiServer: settings.apiServer,
partnerApiServer: settings.partnerApiServer,
isRegistered: settings.isRegistered,
@@ -652,7 +664,8 @@ export default class Help extends Vue {
});
// Update component state
this.activeDid = settings.activeDid || "";
this.activeDid = activeIdentity.activeDid || "";
this.apiServer = settings.apiServer || "";
this.partnerApiServer = settings.partnerApiServer || "";
this.userName = settings.firstName;
@@ -957,15 +970,28 @@ export default class Help extends Vue {
* Supports both SELECT queries (dbQuery) and other SQL commands (dbExec)
* Provides interface for testing raw SQL operations
* Uses PlatformServiceMixin for database access and notification helpers for errors
* When returnRawResults is true, uses direct platform service methods for unparsed results
*/
async executeSql() {
try {
const isSelect = this.sqlQuery.trim().toLowerCase().startsWith("select");
if (isSelect) {
this.sqlResult = await this.$query(this.sqlQuery);
if (this.returnRawResults) {
// Use direct platform service methods for raw, unparsed results
if (isSelect) {
this.sqlResult = await this.$dbRawQuery(this.sqlQuery);
} else {
this.sqlResult = await this.$exec(this.sqlQuery);
}
} else {
this.sqlResult = await this.$exec(this.sqlQuery);
// Use methods that normalize the result objects
if (isSelect) {
this.sqlResult = await this.$query(this.sqlQuery);
} else {
this.sqlResult = await this.$exec(this.sqlQuery);
}
}
logger.log("Test SQL Result:", this.sqlResult);
} catch (error) {
logger.error("Test SQL Error:", error);
@@ -991,7 +1017,7 @@ export default class Help extends Vue {
this.urlTestResults = [];
try {
logger.info("[TestView] 🔬 Starting comprehensive URL flow test");
logger.debug("[TestView] 🔬 Starting comprehensive URL flow test");
this.addUrlTestResult("🚀 Starting URL flow test...");
// Test 1: Current state
@@ -1119,7 +1145,7 @@ export default class Help extends Vue {
);
this.addUrlTestResult(`\n✅ URL flow test completed successfully!`);
logger.info("[TestView] ✅ URL flow test completed successfully");
logger.debug("[TestView] ✅ URL flow test completed successfully");
} catch (error) {
const errorMsg = `❌ URL flow test failed: ${error instanceof Error ? error.message : String(error)}`;
this.addUrlTestResult(errorMsg);

View File

@@ -183,7 +183,12 @@ export default class UserProfileView 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.partnerApiServer = settings.partnerApiServer || this.partnerApiServer;
}