diff --git a/package-lock.json b/package-lock.json index 579ccd38..4e5c1b11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "timesafari", - "version": "0.4.6", + "version": "0.4.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "timesafari", - "version": "0.4.6", + "version": "0.4.7", "dependencies": { "@capacitor-community/sqlite": "6.0.2", "@capacitor-mlkit/barcode-scanning": "^6.0.0", diff --git a/src/db/databaseUtil.ts b/src/db/databaseUtil.ts index a0fdd87e..69c80b05 100644 --- a/src/db/databaseUtil.ts +++ b/src/db/databaseUtil.ts @@ -79,10 +79,13 @@ const DEFAULT_SETTINGS: Settings = { // retrieves default settings export async function retrieveSettingsForDefaultAccount(): Promise { + console.log("[databaseUtil] retrieveSettingsForDefaultAccount"); const platform = PlatformServiceFactory.getInstance(); - const result = await platform.dbQuery("SELECT * FROM settings WHERE id = ?", [ - MASTER_SETTINGS_KEY, - ]); + const sql = "SELECT * FROM settings WHERE id = ?"; + console.log("[databaseUtil] sql", sql); + const result = await platform.dbQuery(sql, [MASTER_SETTINGS_KEY]); + console.log("[databaseUtil] result", JSON.stringify(result, null, 2)); + console.trace("Trace from [retrieveSettingsForDefaultAccount]"); if (!result) { return DEFAULT_SETTINGS; } else { @@ -98,28 +101,86 @@ export async function retrieveSettingsForDefaultAccount(): Promise { } } +/** + * Retrieves settings for the active account, merging with default settings + * + * @returns Promise Combined settings with account-specific overrides + * @throws Will log specific errors for debugging but returns default settings on failure + */ export async function retrieveSettingsForActiveAccount(): Promise { - const defaultSettings = await retrieveSettingsForDefaultAccount(); - if (!defaultSettings.activeDid) { - return defaultSettings; - } else { - const platform = PlatformServiceFactory.getInstance(); - const result = await platform.dbQuery( - "SELECT * FROM settings WHERE accountDid = ?", - [defaultSettings.activeDid], - ); - const overrideSettings = result - ? (mapColumnsToValues(result.columns, result.values)[0] as Settings) - : {}; - const overrideSettingsFiltered = Object.fromEntries( - Object.entries(overrideSettings).filter(([_, v]) => v !== null), - ); - const settings = { ...defaultSettings, ...overrideSettingsFiltered }; - if (settings.searchBoxes) { - // @ts-expect-error - the searchBoxes field is a string in the DB - settings.searchBoxes = JSON.parse(settings.searchBoxes); + logConsoleAndDb("[databaseUtil] Starting settings retrieval for active account"); + + try { + // Get default settings first + const defaultSettings = await retrieveSettingsForDefaultAccount(); + logConsoleAndDb(`[databaseUtil] Retrieved default settings (hasActiveDid: ${!!defaultSettings.activeDid})`); + + // If no active DID, return defaults + if (!defaultSettings.activeDid) { + logConsoleAndDb("[databaseUtil] No active DID found, returning default settings"); + return defaultSettings; } - return settings; + + // Get account-specific settings + try { + const platform = PlatformServiceFactory.getInstance(); + const result = await platform.dbQuery( + "SELECT * FROM settings WHERE accountDid = ?", + [defaultSettings.activeDid], + ); + + if (!result?.values?.length) { + logConsoleAndDb(`[databaseUtil] No account-specific settings found for ${defaultSettings.activeDid}`); + return defaultSettings; + } + + // Map and filter settings + const overrideSettings = mapColumnsToValues(result.columns, result.values)[0] as Settings; + const overrideSettingsFiltered = Object.fromEntries( + Object.entries(overrideSettings).filter(([_, v]) => v !== null), + ); + + // Merge settings + const settings = { ...defaultSettings, ...overrideSettingsFiltered }; + + // Handle searchBoxes parsing + if (settings.searchBoxes) { + try { + // @ts-expect-error - the searchBoxes field is a string in the DB + settings.searchBoxes = JSON.parse(settings.searchBoxes); + } catch (error) { + logConsoleAndDb( + `[databaseUtil] Failed to parse searchBoxes for ${defaultSettings.activeDid}: ${error}`, + true + ); + // Reset to empty array on parse failure + settings.searchBoxes = []; + } + } + + logConsoleAndDb( + `[databaseUtil] Successfully merged settings for ${defaultSettings.activeDid} ` + + `(overrides: ${Object.keys(overrideSettingsFiltered).length})` + ); + return settings; + + } catch (error) { + logConsoleAndDb( + `[databaseUtil] Failed to retrieve account settings for ${defaultSettings.activeDid}: ${error}`, + true + ); + // Return defaults on error + return defaultSettings; + } + + } catch (error) { + logConsoleAndDb(`[databaseUtil] Failed to retrieve default settings: ${error}`, true); + // Return minimal default settings on complete failure + return { + id: MASTER_SETTINGS_KEY, + activeDid: undefined, + apiServer: DEFAULT_ENDORSER_API_SERVER, + }; } } diff --git a/src/libs/util.ts b/src/libs/util.ts index b93f38c5..035bd164 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -548,14 +548,20 @@ export const retrieveAccountMetadata = async ( }; export const retrieveAllAccountsMetadata = async (): Promise => { + console.log("[retrieveAllAccountsMetadata] start"); const platformService = PlatformServiceFactory.getInstance(); - const dbAccounts = await platformService.dbQuery(`SELECT * FROM accounts`); + const sql = `SELECT * FROM accounts`; + console.log("[retrieveAllAccountsMetadata] sql: ", sql); + const dbAccounts = await platformService.dbQuery(sql); + console.log("[retrieveAllAccountsMetadata] dbAccounts: ", dbAccounts); const accounts = databaseUtil.mapQueryResultToValues(dbAccounts) as Account[]; let result = accounts.map((account) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { identity, mnemonic, ...metadata } = account; return metadata as Account; }); + console.log("[retrieveAllAccountsMetadata] result: ", result); + console.log("[retrieveAllAccountsMetadata] USE_DEXIE_DB: ", USE_DEXIE_DB); if (USE_DEXIE_DB) { // one of the few times we use accountsDBPromise directly; try to avoid more usage const accountsDB = await accountsDBPromise; @@ -566,6 +572,7 @@ export const retrieveAllAccountsMetadata = async (): Promise => { return metadata as Account; }); } + console.log("[retrieveAllAccountsMetadata] end", JSON.stringify(result, null, 2)); return result; }; @@ -646,6 +653,10 @@ export async function saveNewIdentity( derivationPath: string, ): Promise { try { + console.log("[saveNewIdentity] identity", identity); + console.log("[saveNewIdentity] mnemonic", mnemonic); + console.log("[saveNewIdentity] newId", newId); + console.log("[saveNewIdentity] derivationPath", derivationPath); // add to the new sql db const platformService = PlatformServiceFactory.getInstance(); const secrets = await platformService.dbQuery( @@ -662,18 +673,19 @@ export async function saveNewIdentity( const encryptedMnemonic = await simpleEncrypt(mnemonic, secret); const encryptedIdentityBase64 = arrayBufferToBase64(encryptedIdentity); const encryptedMnemonicBase64 = arrayBufferToBase64(encryptedMnemonic); - await platformService.dbExec( - `INSERT INTO accounts (dateCreated, derivationPath, did, identityEncrBase64, mnemonicEncrBase64, publicKeyHex) - VALUES (?, ?, ?, ?, ?, ?)`, - [ - new Date().toISOString(), - derivationPath, - newId.did, - encryptedIdentityBase64, - encryptedMnemonicBase64, - newId.keys[0].publicKeyHex, - ], - ); + const sql = `INSERT INTO accounts (dateCreated, derivationPath, did, identityEncrBase64, mnemonicEncrBase64, publicKeyHex) + VALUES (?, ?, ?, ?, ?, ?)`; + console.log("[saveNewIdentity] sql: ", sql); + const params = [ + new Date().toISOString(), + derivationPath, + newId.did, + encryptedIdentityBase64, + encryptedMnemonicBase64, + newId.keys[0].publicKeyHex, + ]; + console.log("[saveNewIdentity] params: ", params); + await platformService.dbExec(sql, params); await databaseUtil.updateDefaultSettings({ activeDid: newId.did }); if (USE_DEXIE_DB) { @@ -690,6 +702,7 @@ export async function saveNewIdentity( await updateDefaultSettings({ activeDid: newId.did }); } } catch (error) { + console.log("[saveNewIdentity] error: ", error); logger.error("Failed to update default settings:", error); throw new Error( "Failed to set default settings. Please try again or restart the app.", diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts index cbf83f79..1ce0bca7 100644 --- a/src/services/platforms/CapacitorPlatformService.ts +++ b/src/services/platforms/CapacitorPlatformService.ts @@ -128,6 +128,8 @@ export class CapacitorPlatformService implements PlatformService { let result: unknown; switch (operation.type) { case "run": { + console.log("[CapacitorPlatformService] running sql:", operation.sql); + console.log("[CapacitorPlatformService] params:", operation.params); const runResult = await this.db.run( operation.sql, operation.params, @@ -139,6 +141,8 @@ export class CapacitorPlatformService implements PlatformService { break; } case "query": { + console.log("[CapacitorPlatformService] querying sql:", operation.sql); + console.log("[CapacitorPlatformService] params:", operation.params); const queryResult = await this.db.query( operation.sql, operation.params, diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index 22f342f1..66e4bf74 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -1119,6 +1119,7 @@ export default class AccountViewView extends Vue { */ async mounted() { try { + console.log("[AccountViewView] mounted"); // Initialize component state with values from the database or defaults await this.initializeState(); await this.processIdentity(); @@ -1171,6 +1172,7 @@ export default class AccountViewView extends Vue { } } } catch (error) { + console.log("[AccountViewView] error: ", JSON.stringify(error, null, 2)); // this can happen when running automated tests in dev mode because notifications don't work logger.error( "Telling user to clear cache at page create because:", diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 4bd03745..e42f0929 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -515,49 +515,85 @@ export default class HomeView extends Vue { */ private async initializeIdentity() { try { - this.allMyDids = await retrieveAccountDids(); + // Retrieve DIDs with better error handling + try { + this.allMyDids = await retrieveAccountDids(); + logConsoleAndDb(`[HomeView] Retrieved ${this.allMyDids.length} DIDs`); + } catch (error) { + logConsoleAndDb(`[HomeView] Failed to retrieve DIDs: ${error}`, true); + throw new Error("Failed to load existing identities. Please try restarting the app."); + } + + // Create new DID if needed if (this.allMyDids.length === 0) { - this.isCreatingIdentifier = true; - const newDid = await generateSaveAndActivateIdentity(); - this.isCreatingIdentifier = false; - this.allMyDids = [newDid]; + try { + this.isCreatingIdentifier = true; + const newDid = await generateSaveAndActivateIdentity(); + this.isCreatingIdentifier = false; + this.allMyDids = [newDid]; + logConsoleAndDb(`[HomeView] Created new identity: ${newDid}`); + } catch (error) { + this.isCreatingIdentifier = false; + logConsoleAndDb(`[HomeView] Failed to create new identity: ${error}`, true); + throw new Error("Failed to create new identity. Please try again."); + } } - let settings = await databaseUtil.retrieveSettingsForActiveAccount(); - if (USE_DEXIE_DB) { - settings = await retrieveSettingsForActiveAccount(); + // Load settings with better error context + let settings; + try { + settings = await databaseUtil.retrieveSettingsForActiveAccount(); + if (USE_DEXIE_DB) { + settings = await retrieveSettingsForActiveAccount(); + } + logConsoleAndDb(`[HomeView] Retrieved settings for ${settings.activeDid || 'no active DID'}`); + } catch (error) { + logConsoleAndDb(`[HomeView] Failed to retrieve settings: ${error}`, true); + throw new Error("Failed to load user settings. Some features may be limited."); } + + // Update component state this.apiServer = settings.apiServer || ""; this.activeDid = settings.activeDid || ""; - const platformService = PlatformServiceFactory.getInstance(); - const dbContacts = await platformService.dbQuery( - "SELECT * FROM contacts", - ); - this.allContacts = databaseUtil.mapQueryResultToValues( - dbContacts, - ) as unknown as Contact[]; - if (USE_DEXIE_DB) { - this.allContacts = await db.contacts.toArray(); + + // Load contacts with graceful fallback + try { + const platformService = PlatformServiceFactory.getInstance(); + const dbContacts = await platformService.dbQuery("SELECT * FROM contacts"); + this.allContacts = databaseUtil.mapQueryResultToValues(dbContacts) as Contact[]; + if (USE_DEXIE_DB) { + this.allContacts = await db.contacts.toArray(); + } + logConsoleAndDb(`[HomeView] Retrieved ${this.allContacts.length} contacts`); + } catch (error) { + logConsoleAndDb(`[HomeView] Failed to retrieve contacts: ${error}`, true); + this.allContacts = []; // Ensure we have a valid empty array + this.$notify({ + group: "alert", + type: "warning", + title: "Contact Loading Issue", + text: "Some contact information may be unavailable.", + }, 5000); } + + // Update remaining settings this.feedLastViewedClaimId = settings.lastViewedClaimId; this.givenName = settings.firstName || ""; this.isFeedFilteredByVisible = !!settings.filterFeedByVisible; this.isFeedFilteredByNearby = !!settings.filterFeedByNearby; this.isRegistered = !!settings.isRegistered; this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId; - this.lastAckedOfferToUserProjectsJwtId = - settings.lastAckedOfferToUserProjectsJwtId; + this.lastAckedOfferToUserProjectsJwtId = settings.lastAckedOfferToUserProjectsJwtId; this.searchBoxes = settings.searchBoxes || []; this.showShortcutBvc = !!settings.showShortcutBvc; this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings); + // Check onboarding status if (!settings.finishedOnboarding) { - (this.$refs.onboardingDialog as OnboardingDialog).open( - OnboardPage.Home, - ); + (this.$refs.onboardingDialog as OnboardingDialog).open(OnboardPage.Home); } - // someone may have have registered after sharing contact info, so recheck + // Check registration status if needed if (!this.isRegistered && this.activeDid) { try { const resp = await fetchEndorserRateLimits( @@ -577,51 +613,62 @@ export default class HomeView extends Vue { }); } this.isRegistered = true; + logConsoleAndDb(`[HomeView] User ${this.activeDid} is now registered`); } - } catch (e) { - // ignore the error... just keep us unregistered + } catch (error) { + logConsoleAndDb(`[HomeView] Registration check failed: ${error}`, true); + // Continue as unregistered - this is expected for new users } } - // this returns a Promise but we don't need to wait for it - this.updateAllFeed(); - - if (this.activeDid) { - const offersToUserData = await getNewOffersToUser( - this.axios, - this.apiServer, - this.activeDid, - this.lastAckedOfferToUserJwtId, - ); - this.numNewOffersToUser = offersToUserData.data.length; - this.newOffersToUserHitLimit = offersToUserData.hitLimit; - } + // Initialize feed and offers + try { + // Start feed update in background + this.updateAllFeed().catch(error => { + logConsoleAndDb(`[HomeView] Background feed update failed: ${error}`, true); + }); - if (this.activeDid) { - const offersToUserProjects = await getNewOffersToUserProjects( - this.axios, - this.apiServer, - this.activeDid, - this.lastAckedOfferToUserProjectsJwtId, - ); - this.numNewOffersToUserProjects = offersToUserProjects.data.length; - this.newOffersToUserProjectsHitLimit = offersToUserProjects.hitLimit; + // Load new offers if we have an active DID + if (this.activeDid) { + const [offersToUser, offersToProjects] = await Promise.all([ + getNewOffersToUser( + this.axios, + this.apiServer, + this.activeDid, + this.lastAckedOfferToUserJwtId, + ), + getNewOffersToUserProjects( + this.axios, + this.apiServer, + this.activeDid, + this.lastAckedOfferToUserProjectsJwtId, + ), + ]); + + this.numNewOffersToUser = offersToUser.data.length; + this.newOffersToUserHitLimit = offersToUser.hitLimit; + this.numNewOffersToUserProjects = offersToProjects.data.length; + this.newOffersToUserProjectsHitLimit = offersToProjects.hitLimit; + + logConsoleAndDb( + `[HomeView] Retrieved ${this.numNewOffersToUser} user offers and ` + + `${this.numNewOffersToUserProjects} project offers` + ); + } + } catch (error) { + logConsoleAndDb(`[HomeView] Failed to initialize feed/offers: ${error}`, true); + // Don't throw - we can continue with empty feed + this.$notify({ + group: "alert", + type: "warning", + title: "Feed Loading Issue", + text: "Some feed data may be unavailable. Pull to refresh.", + }, 5000); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (err: any) { - logConsoleAndDb("Error retrieving settings or feed: " + err, true); - this.$notify( - { - group: "alert", - type: "danger", - title: "Error", - text: - (err as { userMessage?: string })?.userMessage || - "There was an error retrieving your settings or the latest activity.", - }, - 5000, - ); + } catch (error) { + this.handleError(error); + throw error; // Re-throw to be caught by mounted() } } @@ -784,19 +831,24 @@ export default class HomeView extends Vue { * - Displays user notification * * @internal - * Called by mounted() + * Called by mounted() and initializeIdentity() * @param err Error object with optional userMessage */ private handleError(err: unknown) { - logConsoleAndDb("Error retrieving settings or feed: " + err, true); + const errorMessage = err instanceof Error ? err.message : String(err); + const userMessage = (err as { userMessage?: string })?.userMessage; + + logConsoleAndDb( + `[HomeView] Initialization error: ${errorMessage}${userMessage ? ` (${userMessage})` : ''}`, + true + ); + this.$notify( { group: "alert", type: "danger", title: "Error", - text: - (err as { userMessage?: string })?.userMessage || - "There was an error retrieving your settings or the latest activity.", + text: userMessage || "There was an error loading your data. Please try refreshing the page.", }, 5000, ); diff --git a/src/views/IdentitySwitcherView.vue b/src/views/IdentitySwitcherView.vue index 143506e8..cfb6908b 100644 --- a/src/views/IdentitySwitcherView.vue +++ b/src/views/IdentitySwitcherView.vue @@ -115,6 +115,7 @@ import { MASTER_SETTINGS_KEY } from "../db/tables/settings"; import * as databaseUtil from "../db/databaseUtil"; import { retrieveAllAccountsMetadata } from "../libs/util"; import { logger } from "../utils/logger"; +import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; @Component({ components: { QuickNav } }) export default class IdentitySwitcherView extends Vue { @@ -138,8 +139,10 @@ export default class IdentitySwitcherView extends Vue { this.apiServerInput = settings.apiServer || ""; const accounts = await retrieveAllAccountsMetadata(); + console.log("[IdentitySwitcherView] accounts: ", JSON.stringify(accounts, null, 2)); for (let n = 0; n < accounts.length; n++) { const acct = accounts[n]; + console.log("[IdentitySwitcherView] acct: ", JSON.stringify(acct, null, 2)); this.otherIdentities.push({ id: (acct.id ?? 0).toString(), did: acct.did, @@ -149,6 +152,7 @@ export default class IdentitySwitcherView extends Vue { } } } catch (err) { + console.log("[IdentitySwitcherView] error: ", JSON.stringify(err, null, 2)); this.$notify( { group: "alert", @@ -160,6 +164,7 @@ export default class IdentitySwitcherView extends Vue { ); logger.error("Telling user to clear cache at page create because:", err); } + console.log("[IdentitySwitcherView] end"); } async switchAccount(did?: string) { @@ -167,10 +172,18 @@ export default class IdentitySwitcherView extends Vue { if (did === "0") { did = undefined; } - await db.open(); - await db.settings.update(MASTER_SETTINGS_KEY, { - activeDid: did, - }); + if (USE_DEXIE_DB) { + await db.open(); + await db.settings.update(MASTER_SETTINGS_KEY, { + activeDid: did ?? "", + }); + } else { + const platformService = PlatformServiceFactory.getInstance(); + await platformService.dbExec( + `UPDATE settings SET activeDid = ? WHERE id = ?`, + [did ?? "", MASTER_SETTINGS_KEY], + ); + } this.$router.push({ name: "account" }); }