From 6f5661d61c583a35abafb6a7cc923c2ad6d06d57 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Sun, 24 Aug 2025 17:44:15 -0600 Subject: [PATCH 01/20] fix: enhance the message & provide link on confirmation page when something isn't seen --- src/views/QuickActionBvcEndView.vue | 42 ++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/src/views/QuickActionBvcEndView.vue b/src/views/QuickActionBvcEndView.vue index f02af128..f3c0969d 100644 --- a/src/views/QuickActionBvcEndView.vue +++ b/src/views/QuickActionBvcEndView.vue @@ -69,10 +69,17 @@
{{ claimCountWithHiddenText }} - so if you expected but do not see details from someone then ask them to - check that their activity is visible to you on their Contacts - - page. + If you don't see expected info above for someone, ask them to check that + their activity is visible ( + + + ) to you on + + this page .
@@ -120,10 +127,11 @@ import { DateTime } from "luxon"; import * as R from "ramda"; import { Component, Vue } from "vue-facing-decorator"; import { Router } from "vue-router"; +import { useClipboard } from "@vueuse/core"; import QuickNav from "../components/QuickNav.vue"; import TopMessage from "../components/TopMessage.vue"; -import { NotificationIface } from "../constants/app"; +import { NotificationIface, APP_SERVER } from "../constants/app"; import { Contact } from "../db/tables/contacts"; import { GenericCredWrapper, @@ -148,6 +156,7 @@ import { NOTIFY_ALL_CONFIRMATIONS_ERROR, NOTIFY_GIVE_SEND_ERROR, NOTIFY_CLAIMS_SEND_ERROR, + NOTIFY_COPIED_TO_CLIPBOARD, createConfirmationSuccessMessage, createCombinedSuccessMessage, } from "@/constants/notifications"; @@ -195,8 +204,8 @@ export default class QuickActionBvcEndView extends Vue { get claimCountWithHiddenText() { if (this.claimCountWithHidden === 0) return ""; return this.claimCountWithHidden === 1 - ? "There is 1 other claim with hidden details," - : `There are ${this.claimCountWithHidden} other claims with hidden details,`; + ? "There is 1 other claim with hidden details." + : `There are ${this.claimCountWithHidden} other claims with hidden details.`; } get claimCountByUserText() { @@ -295,6 +304,25 @@ export default class QuickActionBvcEndView extends Vue { (this.$router as Router).push(route); } + copyContactsLinkToClipboard() { + const deepLinkUrl = `${APP_SERVER}/deep-link/did/${this.activeDid}`; + useClipboard() + .copy(deepLinkUrl) + .then(() => { + this.notify.success( + NOTIFY_COPIED_TO_CLIPBOARD.message("Your info link"), + TIMEOUTS.SHORT, + ); + }) + .catch((error) => { + logger.error("Failed to copy to clipboard:", error); + this.notify.error( + "Failed to copy link to clipboard. Please try again.", + TIMEOUTS.SHORT, + ); + }); + } + async record() { try { if (this.claimsToConfirmSelected.length > 0) { From 8991b36a56b12f112a282cddcb9b77ccc1b02f9f Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Sun, 24 Aug 2025 17:49:01 -0600 Subject: [PATCH 02/20] fix: give consistent "you" verbiage on button --- src/views/DIDView.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/views/DIDView.vue b/src/views/DIDView.vue index a6212ece..1e6860f6 100644 --- a/src/views/DIDView.vue +++ b/src/views/DIDView.vue @@ -95,7 +95,7 @@ contactFromDid.did !== activeDid " class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md" - title="I view their content" + title="You view their content" @click="confirmViewContent(contactFromDid, false)" > @@ -107,7 +107,7 @@ contactFromDid?.did !== activeDid " class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md" - title="I do not view their content" + title="You do not view their content" @click="confirmViewContent(contactFromDid, true)" > From 528a68ef6ce337780e25d63fba46386ba293a76b Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Sun, 24 Aug 2025 18:15:08 -0600 Subject: [PATCH 03/20] fix: reorder and reword visibility messages on confirmation & DID view pages --- src/views/DIDView.vue | 16 ++++++++-------- src/views/QuickActionBvcEndView.vue | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/views/DIDView.vue b/src/views/DIDView.vue index 1e6860f6..0b15a604 100644 --- a/src/views/DIDView.vue +++ b/src/views/DIDView.vue @@ -71,22 +71,22 @@ contactFromDid?.seesMe && contactFromDid.did !== activeDid " class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md" - title="They can see you" + title="They can see your activity" @click="confirmSetVisibility(contactFromDid, false)" > - +
-

Given To This Project

+

+ Given To This Project +

(None yet. If you've seen something, say something by clicking a @@ -498,7 +506,9 @@ Benefitted From This Project -
(None yet.)
+
+ (None yet.) +
  • { // Verify duplicate import shows warning message // The warning can appear either from the pre-check or from the saveNewIdentity error handling - await expect(page.getByText("This account has already been imported")).toBeVisible(); - await expect(page.getByText("Please use a different seed phrase or check your existing accounts")).toBeVisible(); + await expect(page.getByText(NOTIFY_DUPLICATE_ACCOUNT_IMPORT.message)).toBeVisible(); // Verify we're still on the import page (not redirected to account) await expect(page.getByPlaceholder("Seed Phrase")).toBeVisible(); @@ -58,6 +58,6 @@ test.describe('Duplicate Account Import', () => { // Verify second import was successful (should not show duplicate warning) await expect(page.getByRole("code")).toContainText(userOneData.did); - await expect(page.getByText("This account has already been imported")).not.toBeVisible(); + await expect(page.getByText(NOTIFY_DUPLICATE_ACCOUNT_IMPORT.message)).not.toBeVisible(); }); }); From 96e4d3c394d466bb56d4745352ee19cc784cbaa2 Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Thu, 28 Aug 2025 18:34:38 +0800 Subject: [PATCH 07/20] chore - reorder duplication test - Rename the test to run it earlier in the test suite --- ...icate-import-test.spec.ts => 03-duplicate-import-test.spec.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test-playwright/{duplicate-import-test.spec.ts => 03-duplicate-import-test.spec.ts} (100%) diff --git a/test-playwright/duplicate-import-test.spec.ts b/test-playwright/03-duplicate-import-test.spec.ts similarity index 100% rename from test-playwright/duplicate-import-test.spec.ts rename to test-playwright/03-duplicate-import-test.spec.ts From 40fa38a9cef7bb1fab31e380cb37c65a36cde6ce Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Thu, 28 Aug 2025 20:50:57 +0800 Subject: [PATCH 08/20] fix: clean up "register random person" test - Remove redundant "import User Zero" action - Remove out-of-scope actions from test (sending a gift to an unrelated entity, deleting the contact) - Update imports based on changes --- test-playwright/00-noid-tests.spec.ts | 50 +++++++++------------------ test-playwright/testUtils.ts | 26 +------------- 2 files changed, 18 insertions(+), 58 deletions(-) diff --git a/test-playwright/00-noid-tests.spec.ts b/test-playwright/00-noid-tests.spec.ts index e84949df..3bda4a27 100644 --- a/test-playwright/00-noid-tests.spec.ts +++ b/test-playwright/00-noid-tests.spec.ts @@ -69,8 +69,7 @@ */ import { test, expect } from '@playwright/test'; -import { UNNAMED_ENTITY_NAME } from '../src/constants/entities'; -import { deleteContact, generateAndRegisterEthrUser, importUser } from './testUtils'; +import { createContactName, generateNewEthrUser, importUser } from './testUtils'; import { NOTIFY_CONTACT_INVALID_DID } from '../src/constants/notifications'; test('Check activity feed - check that server is running', async ({ page }) => { @@ -185,35 +184,20 @@ test('Check invalid DID shows error and redirects', async ({ page }) => { }); test('Check User 0 can register a random person', async ({ page }) => { - await importUser(page, '00'); - const newDid = await generateAndRegisterEthrUser(page); - expect(newDid).toContain('did:ethr:'); - - await page.goto('./'); - await page.getByTestId('closeOnboardingAndFinish').click(); - await page.getByRole('button', { name: 'Person' }).click(); - await page.getByRole('listitem').filter({ hasText: UNNAMED_ENTITY_NAME }).locator('svg').click(); - await page.getByPlaceholder('What was given').fill('Gave me access!'); - await page.getByRole('button', { name: 'Sign & Send' }).click(); - await expect(page.getByText('That gift was recorded.')).toBeVisible(); - // now ensure that alert goes away - await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss alert - await expect(page.getByText('That gift was recorded.')).toBeHidden(); - - // now delete the contact to test that pages still do reasonable things - await deleteContact(page, newDid); - // go the activity page for this new person - await page.goto('./did/' + encodeURIComponent(newDid)); - // maybe replace by: const popupPromise = page.waitForEvent('popup'); - let error; - try { - await page.waitForSelector('div[role="alert"]', { timeout: 2000 }); - error = new Error('Error alert should not show.'); - } catch (error) { - // success - } finally { - if (error) { - throw error; - } - } + const newDid = await generateNewEthrUser(page); // generate a new user + + await importUser(page, "00"); // switch to User Zero + + // As User Zero, add the new user as a contact + await page.goto('./contacts'); + const contactName = createContactName(newDid); + await page.getByPlaceholder('URL or DID, Name, Public Key').fill(`${newDid}, ${contactName}`); + await expect(page.locator('button > svg.fa-plus')).toBeVisible(); + await page.locator('button > svg.fa-plus').click(); + await expect(page.locator('div[role="alert"] h4:has-text("Success")')).toBeVisible(); // wait for info alert to be visible… + await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // …and dismiss it + await expect(page.locator('div[role="alert"] button > svg.fa-xmark')).toBeHidden(); // ensure alert is gone + await page.locator('div[role="alert"] button:text-is("Yes")').click(); // Register new contact + await page.locator('div[role="alert"] button:text-is("No, Not Now")').click(); // Dismiss export data prompt + await expect(page.locator("li", { hasText: contactName })).toBeVisible(); }); diff --git a/test-playwright/testUtils.ts b/test-playwright/testUtils.ts index c82be500..6d232a99 100644 --- a/test-playwright/testUtils.ts +++ b/test-playwright/testUtils.ts @@ -109,7 +109,7 @@ export async function switchToUser(page: Page, did: string): Promise { await page.getByTestId("didWrapper").locator('code:has-text("did:")'); } -function createContactName(did: string): string { +export function createContactName(did: string): string { return "User " + did.slice(11, 14); } @@ -144,30 +144,6 @@ export async function generateNewEthrUser(page: Page): Promise { return newDid; } -// Generate a new random user and register them. -// Note that this makes 000 the active user. Use switchToUser to switch to this DID. -export async function generateAndRegisterEthrUser(page: Page): Promise { - const newDid = await generateNewEthrUser(page); - - await importUser(page, "000"); // switch to user 000 - - await page.goto("./contacts"); - const contactName = createContactName(newDid); - await page - .getByPlaceholder("URL or DID, Name, Public Key") - .fill(`${newDid}, ${contactName}`); - await page.locator("button > svg.fa-plus").click(); - // register them - await page.locator('div[role="alert"] button:text-is("Yes")').click(); - // wait for it to disappear because the next steps may depend on alerts being gone - await expect( - page.locator('div[role="alert"] button:text-is("Yes")') - ).toBeHidden(); - await expect(page.locator("li", { hasText: contactName })).toBeVisible(); - - return newDid; -} - // Function to generate a random string of specified length export async function generateRandomString(length: number): Promise { return Math.random() From e67c97821a4d59a245524c814a4e06e31274344b Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Thu, 28 Aug 2025 21:06:26 +0800 Subject: [PATCH 09/20] fix: change import User Zero function - Use the ./account route to mimic real-world use --- test-playwright/00-noid-tests.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-playwright/00-noid-tests.spec.ts b/test-playwright/00-noid-tests.spec.ts index 3bda4a27..8da08e33 100644 --- a/test-playwright/00-noid-tests.spec.ts +++ b/test-playwright/00-noid-tests.spec.ts @@ -69,7 +69,7 @@ */ import { test, expect } from '@playwright/test'; -import { createContactName, generateNewEthrUser, importUser } from './testUtils'; +import { createContactName, generateNewEthrUser, importUser, importUserFromAccount } from './testUtils'; import { NOTIFY_CONTACT_INVALID_DID } from '../src/constants/notifications'; test('Check activity feed - check that server is running', async ({ page }) => { @@ -186,7 +186,7 @@ test('Check invalid DID shows error and redirects', async ({ page }) => { test('Check User 0 can register a random person', async ({ page }) => { const newDid = await generateNewEthrUser(page); // generate a new user - await importUser(page, "00"); // switch to User Zero + await importUserFromAccount(page, "00"); // switch to User Zero // As User Zero, add the new user as a contact await page.goto('./contacts'); From 83c0c18db2c5fd0b52bf8fff3223d820685eadb9 Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Fri, 29 Aug 2025 16:41:19 +0800 Subject: [PATCH 10/20] fix: persist identity names per user instead of globally Fixes issue where identity names were not saved when switching between multiple identities. Names were being saved to master settings instead of user-specific settings. Changes: - UserNameDialog: Load/save names from/to user-specific settings - NewEditAccountView: Save names to user-specific settings for active DID - Both components now use $accountSettings() and $saveUserSettings() instead of $settings() and $updateSettings() Each identity now properly retains their assigned name when switching between identities. Previously only "User Zero" would show their name due to using master settings instead of per-identity settings. Fixes: Identity name persistence across identity switches --- src/components/UserNameDialog.vue | 16 ++++++++++++++-- src/views/NewEditAccountView.vue | 22 ++++++++++++++++++---- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/components/UserNameDialog.vue b/src/components/UserNameDialog.vue index d71ee9fc..47607dac 100644 --- a/src/components/UserNameDialog.vue +++ b/src/components/UserNameDialog.vue @@ -84,7 +84,8 @@ export default class UserNameDialog extends Vue { */ async open(aCallback?: (name?: string) => void) { this.callback = aCallback || this.callback; - const settings = await this.$settings(); + // Load from account-specific settings instead of master settings + const settings = await this.$accountSettings(); this.givenName = settings.firstName || ""; this.visible = true; } @@ -95,7 +96,18 @@ export default class UserNameDialog extends Vue { */ async onClickSaveChanges() { try { - await this.$updateSettings({ firstName: this.givenName }); + // Get the current active DID to save to user-specific settings + const settings = await this.$accountSettings(); + const activeDid = settings.activeDid; + + if (activeDid) { + // Save to user-specific settings for the current identity + await this.$saveUserSettings(activeDid, { firstName: this.givenName }); + } else { + // Fallback to master settings if no active DID + await this.$saveSettings({ firstName: this.givenName }); + } + this.visible = false; this.callback(this.givenName); } catch (error) { diff --git a/src/views/NewEditAccountView.vue b/src/views/NewEditAccountView.vue index 78e709f2..98be3282 100644 --- a/src/views/NewEditAccountView.vue +++ b/src/views/NewEditAccountView.vue @@ -110,10 +110,24 @@ export default class NewEditAccountView extends Vue { * @async */ async onClickSaveChanges() { - await this.$updateSettings({ - firstName: this.givenName, - lastName: "", // deprecated, pre v 0.1.3 - }); + // Get the current active DID to save to user-specific settings + const settings = await this.$accountSettings(); + const activeDid = settings.activeDid; + + if (activeDid) { + // Save to user-specific settings for the current identity + await this.$saveUserSettings(activeDid, { + firstName: this.givenName, + lastName: "", // deprecated, pre v 0.1.3 + }); + } else { + // Fallback to master settings if no active DID + await this.$saveSettings({ + firstName: this.givenName, + lastName: "", // deprecated, pre v 0.1.3 + }); + } + this.$router.back(); } From dde37e73e1a7c9de98fdcab8eb1ae318b43a201b Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Fri, 29 Aug 2025 16:41:46 +0800 Subject: [PATCH 11/20] Lint fixes --- src/components/UserNameDialog.vue | 4 ++-- src/views/NewEditAccountView.vue | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/UserNameDialog.vue b/src/components/UserNameDialog.vue index 47607dac..7a426e7f 100644 --- a/src/components/UserNameDialog.vue +++ b/src/components/UserNameDialog.vue @@ -99,7 +99,7 @@ export default class UserNameDialog extends Vue { // Get the current active DID to save to user-specific settings const settings = await this.$accountSettings(); const activeDid = settings.activeDid; - + if (activeDid) { // Save to user-specific settings for the current identity await this.$saveUserSettings(activeDid, { firstName: this.givenName }); @@ -107,7 +107,7 @@ export default class UserNameDialog extends Vue { // Fallback to master settings if no active DID await this.$saveSettings({ firstName: this.givenName }); } - + this.visible = false; this.callback(this.givenName); } catch (error) { diff --git a/src/views/NewEditAccountView.vue b/src/views/NewEditAccountView.vue index 98be3282..7db96689 100644 --- a/src/views/NewEditAccountView.vue +++ b/src/views/NewEditAccountView.vue @@ -113,7 +113,7 @@ export default class NewEditAccountView extends Vue { // Get the current active DID to save to user-specific settings const settings = await this.$accountSettings(); const activeDid = settings.activeDid; - + if (activeDid) { // Save to user-specific settings for the current identity await this.$saveUserSettings(activeDid, { @@ -127,7 +127,7 @@ export default class NewEditAccountView extends Vue { lastName: "", // deprecated, pre v 0.1.3 }); } - + this.$router.back(); } From 4a1249d1666f1c775f08729bbd3030e3cf34eddc Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Fri, 29 Aug 2025 18:05:37 +0800 Subject: [PATCH 12/20] feat(electron): add editMenu to enable copy/paste keyboard shortcuts - Add 'editMenu' role to AppMenuBarMenuTemplate in setup.ts and index.ts - Enables standard keyboard shortcuts (Cmd+C, Cmd+V, etc.) in Electron app - Fixes issue where copy/paste shortcuts were not working in text inputs - Maintains existing clipboard service functionality for programmatic operations Resolves keyboard shortcut functionality for better user experience in desktop app. --- electron/src/index.ts | 1 + electron/src/setup.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/electron/src/index.ts b/electron/src/index.ts index 3ca3215e..a7712f3d 100644 --- a/electron/src/index.ts +++ b/electron/src/index.ts @@ -50,6 +50,7 @@ process.stderr.on('error', (err) => { const trayMenuTemplate: (MenuItemConstructorOptions | MenuItem)[] = [new MenuItem({ label: 'Quit App', role: 'quit' })]; const appMenuBarMenuTemplate: (MenuItemConstructorOptions | MenuItem)[] = [ { role: process.platform === 'darwin' ? 'appMenu' : 'fileMenu' }, + { role: 'editMenu' }, { role: 'viewMenu' }, ]; diff --git a/electron/src/setup.ts b/electron/src/setup.ts index 55d79f1a..19c2673d 100644 --- a/electron/src/setup.ts +++ b/electron/src/setup.ts @@ -53,6 +53,7 @@ export class ElectronCapacitorApp { ]; private AppMenuBarMenuTemplate: (MenuItem | MenuItemConstructorOptions)[] = [ { role: process.platform === 'darwin' ? 'appMenu' : 'fileMenu' }, + { role: 'editMenu' }, { role: 'viewMenu' }, ]; private mainWindowState; From c9082fa57bc4303d0c707558470f017eb7e56efa Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Mon, 1 Sep 2025 16:02:48 +0800 Subject: [PATCH 13/20] refactor: remove single-use notification constant - Replace constant usage with direct message string in ImportDerivedAccountView.vue - Clean up import statement to remove unused import - Remove unused constant from notifications.ts --- src/constants/notifications.ts | 8 -------- src/views/ImportDerivedAccountView.vue | 3 +-- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/constants/notifications.ts b/src/constants/notifications.ts index 136a6a24..5cc75bd4 100644 --- a/src/constants/notifications.ts +++ b/src/constants/notifications.ts @@ -1697,11 +1697,3 @@ export const NOTIFY_DUPLICATE_ACCOUNT_IMPORT = { message: "This account has already been imported. Please use a different seed phrase or check your existing accounts.", }; - -// ImportDerivedAccountView.vue specific constants -// Used in: ImportDerivedAccountView.vue (incrementDerivation method - duplicate derived account warning) -export const NOTIFY_DUPLICATE_DERIVED_ACCOUNT = { - title: "Derived Account Already Exists", - message: - "This derived account already exists. Please try a different derivation path.", -}; diff --git a/src/views/ImportDerivedAccountView.vue b/src/views/ImportDerivedAccountView.vue index 834950f6..44ef37d2 100644 --- a/src/views/ImportDerivedAccountView.vue +++ b/src/views/ImportDerivedAccountView.vue @@ -91,7 +91,6 @@ import { createNotifyHelpers, TIMEOUTS, NotifyFunction } from "@/utils/notify"; import { NOTIFY_ACCOUNT_DERIVATION_SUCCESS, NOTIFY_ACCOUNT_DERIVATION_ERROR, - NOTIFY_DUPLICATE_DERIVED_ACCOUNT, } from "@/constants/notifications"; @Component({ @@ -176,7 +175,7 @@ export default class ImportAccountView extends Vue { const isDuplicate = await this.checkForDuplicateAccount(newId.did); if (isDuplicate) { this.notify.warning( - NOTIFY_DUPLICATE_DERIVED_ACCOUNT.message, + "This derived account already exists. Please try a different derivation path.", TIMEOUTS.LONG, ); return; From 5f8d1fc8c6529efae09abf69ca24950ae3374a14 Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Mon, 1 Sep 2025 16:54:36 +0800 Subject: [PATCH 14/20] refactor: remove deprecated lastName field from user settings - Remove lastName field from $saveUserSettings and $saveSettings calls - Clean up deprecated pre v0.1.3 code --- src/views/NewEditAccountView.vue | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/views/NewEditAccountView.vue b/src/views/NewEditAccountView.vue index 7db96689..d1349e5a 100644 --- a/src/views/NewEditAccountView.vue +++ b/src/views/NewEditAccountView.vue @@ -118,13 +118,11 @@ export default class NewEditAccountView extends Vue { // Save to user-specific settings for the current identity await this.$saveUserSettings(activeDid, { firstName: this.givenName, - lastName: "", // deprecated, pre v 0.1.3 }); } else { // Fallback to master settings if no active DID await this.$saveSettings({ firstName: this.givenName, - lastName: "", // deprecated, pre v 0.1.3 }); } From d339f1a27401cb8e8dcfb11c18f97f2e117477b0 Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Mon, 1 Sep 2025 19:33:18 +0800 Subject: [PATCH 15/20] chore: remove generated doc - Generated document reads more like a log, and does not contribute to actual documentation of app --- ...duplicate-account-import-implementation.md | 26 ------------------- 1 file changed, 26 deletions(-) delete mode 100644 doc/duplicate-account-import-implementation.md diff --git a/doc/duplicate-account-import-implementation.md b/doc/duplicate-account-import-implementation.md deleted file mode 100644 index ec11e7bb..00000000 --- a/doc/duplicate-account-import-implementation.md +++ /dev/null @@ -1,26 +0,0 @@ -## What Works (Evidence) - -- ✅ **ImportAccountView duplicate check** - - **Time**: 2025-01-27T14:30:00Z - - **Evidence**: Added `checkForDuplicateAccount()` method with DID derivation and database query - - **Verify at**: `src/views/ImportAccountView.vue:180-200` - -- ✅ **ImportAccountView error handling** - - **Time**: 2025-01-27T15:00:00Z - - **Evidence**: Enhanced error handling to catch duplicate errors from saveNewIdentity and display user-friendly warnings - - **Verify at**: `src/views/ImportAccountView.vue:230-240` - -- ✅ **ImportDerivedAccountView duplicate check** - - **Time**: 2025-01-27T14:30:00Z - - **Evidence**: Added duplicate check in `incrementDerivation()` method - - **Verify at**: `src/views/ImportDerivedAccountView.vue:170-190` - -- ✅ **saveNewIdentity safety check** - - **Time**: 2025-01-27T14:30:00Z - - **Evidence**: Added database query before INSERT operation - - **Verify at**: `src/libs/util.ts:625-635` - -- ✅ **User-friendly error messages** - - **Time**: 2025-01-27T14:30:00Z - - **Evidence**: Clear warning messages in both import views - - **Verify at**: ImportAccountView and ImportDerivedAccountView notification calls From 25e37cc41545fafc921ae9c832fe027ec489d1ab Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Mon, 1 Sep 2025 19:36:01 +0800 Subject: [PATCH 16/20] refactor: consolidate duplicate account checking logic into unified utility - Extract checkForDuplicateAccount methods from ImportAccountView and ImportDerivedAccountView - Create unified utility function in src/libs/util.ts with TypeScript overloads - Support both direct DID checking and mnemonic+derivation path checking - Improve error handling with centralized logging via PlatformServiceFactory - Add comprehensive JSDoc documentation for both function overloads - Remove unused imports (deriveAddress, newIdentifier) from ImportAccountView The utility function now provides a clean API: - checkForDuplicateAccount(did) - for direct DID checking - checkForDuplicateAccount(mnemonic, derivationPath) - for derivation + checking Both components maintain identical functionality while using centralized logic. --- src/libs/util.ts | 66 ++++++++++++++++++++++++++ src/views/ImportAccountView.vue | 53 ++++----------------- src/views/ImportDerivedAccountView.vue | 25 +--------- 3 files changed, 77 insertions(+), 67 deletions(-) diff --git a/src/libs/util.ts b/src/libs/util.ts index 83004078..29e8dd82 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -1042,3 +1042,69 @@ export async function importFromMnemonic( } } } + +/** + * Checks if an account with the given DID already exists in the database + * + * @param did - The DID to check for duplicates + * @returns Promise - True if account already exists, false otherwise + * @throws Error if database query fails + */ +export async function checkForDuplicateAccount(did: string): Promise; + +/** + * Checks if an account with the given DID already exists in the database + * + * @param mnemonic - The mnemonic phrase to derive DID from + * @param derivationPath - The derivation path to use + * @returns Promise - True if account already exists, false otherwise + * @throws Error if database query fails + */ +export async function checkForDuplicateAccount( + mnemonic: string, + derivationPath: string, +): Promise; + +/** + * Implementation of checkForDuplicateAccount with overloaded signatures + */ +export async function checkForDuplicateAccount( + didOrMnemonic: string, + derivationPath?: string, +): Promise { + try { + let didToCheck: string; + + if (derivationPath) { + // Derive the DID from mnemonic and derivation path + const [address, privateHex, publicHex] = deriveAddress( + didOrMnemonic.trim().toLowerCase(), + derivationPath, + ); + + const newId = newIdentifier( + address, + privateHex, + publicHex, + derivationPath, + ); + didToCheck = newId.did; + } else { + // Use the provided DID directly + didToCheck = didOrMnemonic; + } + + // Check if an account with this DID already exists + const platformService = await getPlatformService(); + const existingAccount = await platformService.dbQuery( + "SELECT did FROM accounts WHERE did = ?", + [didToCheck], + ); + + return (existingAccount?.values?.length ?? 0) > 0; + } catch (error) { + // If we can't check for duplicates, let the calling process handle the error + logger.error("Error checking for duplicate account:", error); + throw error; + } +} diff --git a/src/views/ImportAccountView.vue b/src/views/ImportAccountView.vue index 4b0c006b..e76ec768 100644 --- a/src/views/ImportAccountView.vue +++ b/src/views/ImportAccountView.vue @@ -87,12 +87,12 @@ import { Component, Vue } from "vue-facing-decorator"; import { Router } from "vue-router"; import { AppString, NotificationIface } from "../constants/app"; +import { DEFAULT_ROOT_DERIVATION_PATH } from "../libs/crypto"; import { - DEFAULT_ROOT_DERIVATION_PATH, - deriveAddress, - newIdentifier, -} from "../libs/crypto"; -import { retrieveAccountCount, importFromMnemonic } from "../libs/util"; + retrieveAccountCount, + importFromMnemonic, + checkForDuplicateAccount, +} from "../libs/util"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify"; import { NOTIFY_DUPLICATE_ACCOUNT_IMPORT } from "@/constants/notifications"; @@ -204,7 +204,10 @@ export default class ImportAccountView extends Vue { try { // Check for duplicate account before importing - const isDuplicate = await this.checkForDuplicateAccount(); + const isDuplicate = await checkForDuplicateAccount( + this.mnemonic, + this.derivationPath, + ); if (isDuplicate) { this.notify.warning( NOTIFY_DUPLICATE_ACCOUNT_IMPORT.message, @@ -259,43 +262,5 @@ export default class ImportAccountView extends Vue { ); } } - - /** - * Checks if the account to be imported already exists - * - * Derives the DID from the mnemonic and checks if it exists in the database - * @returns Promise - True if account already exists, false otherwise - */ - private async checkForDuplicateAccount(): Promise { - try { - // Derive the address and create the identifier to get the DID - const [address, privateHex, publicHex] = deriveAddress( - this.mnemonic.trim().toLowerCase(), - this.derivationPath, - ); - - const newId = newIdentifier( - address, - publicHex, - privateHex, - this.derivationPath, - ); - const didToCheck = newId.did; - - // Check if an account with this DID already exists - const existingAccount = await this.$query( - "SELECT did FROM accounts WHERE did = ?", - [didToCheck], - ); - - return existingAccount?.values?.length > 0; - } catch (error) { - // If we can't check for duplicates (e.g., invalid mnemonic), - // let the import process handle the error - this.$logError("Error checking for duplicate account: " + error); - // Return false to let the import process continue and handle the error - return false; - } - } } diff --git a/src/views/ImportDerivedAccountView.vue b/src/views/ImportDerivedAccountView.vue index 44ef37d2..2772cbe8 100644 --- a/src/views/ImportDerivedAccountView.vue +++ b/src/views/ImportDerivedAccountView.vue @@ -83,6 +83,7 @@ import { retrieveAllAccountsMetadata, retrieveFullyDecryptedAccount, saveNewIdentity, + checkForDuplicateAccount, } from "../libs/util"; import { logger } from "../utils/logger"; import { Account, AccountEncrypted } from "../db/tables/accounts"; @@ -172,7 +173,7 @@ export default class ImportAccountView extends Vue { try { // Check for duplicate account before creating - const isDuplicate = await this.checkForDuplicateAccount(newId.did); + const isDuplicate = await checkForDuplicateAccount(newId.did); if (isDuplicate) { this.notify.warning( "This derived account already exists. Please try a different derivation path.", @@ -202,27 +203,5 @@ export default class ImportAccountView extends Vue { this.notify.error(NOTIFY_ACCOUNT_DERIVATION_ERROR.message, TIMEOUTS.LONG); } } - - /** - * Checks if the account to be created already exists - * - * @param did - The DID to check for duplicates - * @returns Promise - True if account already exists, false otherwise - */ - private async checkForDuplicateAccount(did: string): Promise { - try { - // Check if an account with this DID already exists - const existingAccount = await this.$query( - "SELECT did FROM accounts WHERE did = ?", - [did], - ); - - return existingAccount?.values?.length > 0; - } catch (error) { - // If we can't check for duplicates, let the save process handle the error - this.$logError("Error checking for duplicate account: " + error); - return false; - } - } } From 14992110186940913e24f0e8165b89959d63d4f6 Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Mon, 1 Sep 2025 20:03:17 +0800 Subject: [PATCH 17/20] refactor: simplify duplicate account error detection Replace dual string check with single unique identifier for more precise error handling --- src/views/ImportAccountView.vue | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/views/ImportAccountView.vue b/src/views/ImportAccountView.vue index e76ec768..abceb519 100644 --- a/src/views/ImportAccountView.vue +++ b/src/views/ImportAccountView.vue @@ -245,10 +245,7 @@ export default class ImportAccountView extends Vue { // Check if this is a duplicate account error from saveNewIdentity const errorMessage = error instanceof Error ? error.message : String(error); - if ( - errorMessage.includes("already exists") && - errorMessage.includes("Cannot import duplicate account") - ) { + if (errorMessage.includes("Cannot import duplicate account")) { this.notify.warning( NOTIFY_DUPLICATE_ACCOUNT_IMPORT.message, TIMEOUTS.LONG, From fa8956fb38a347bf9ec70e031f888c88bfae99dc Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Mon, 1 Sep 2025 06:42:00 -0600 Subject: [PATCH 18/20] chore: explicitly share error message used for logic --- src/libs/util.ts | 4 +++- src/views/ImportAccountView.vue | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/libs/util.ts b/src/libs/util.ts index 29e8dd82..f585bf2b 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -614,6 +614,8 @@ export const retrieveAllAccountsMetadata = async (): Promise< return result; }; +export const DUPLICATE_ACCOUNT_ERROR = "Cannot import duplicate account."; + /** * Saves a new identity to both SQL and Dexie databases */ @@ -634,7 +636,7 @@ export async function saveNewIdentity( if (existingAccount?.values?.length) { throw new Error( - `Account with DID ${identity.did} already exists. Cannot import duplicate account.`, + `Account with DID ${identity.did} already exists. ${DUPLICATE_ACCOUNT_ERROR}`, ); } diff --git a/src/views/ImportAccountView.vue b/src/views/ImportAccountView.vue index abceb519..d4588423 100644 --- a/src/views/ImportAccountView.vue +++ b/src/views/ImportAccountView.vue @@ -92,6 +92,7 @@ import { retrieveAccountCount, importFromMnemonic, checkForDuplicateAccount, + DUPLICATE_ACCOUNT_ERROR, } from "../libs/util"; import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin"; import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify"; @@ -245,7 +246,7 @@ export default class ImportAccountView extends Vue { // Check if this is a duplicate account error from saveNewIdentity const errorMessage = error instanceof Error ? error.message : String(error); - if (errorMessage.includes("Cannot import duplicate account")) { + if (errorMessage.includes(DUPLICATE_ACCOUNT_ERROR)) { this.notify.warning( NOTIFY_DUPLICATE_ACCOUNT_IMPORT.message, TIMEOUTS.LONG, From 2c7cb9333eed325a78b905fc4634d4fc7e4de5c4 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Mon, 1 Sep 2025 06:59:36 -0600 Subject: [PATCH 19/20] chore: remove error logging for errors that are propagated --- src/libs/util.ts | 140 +++++++++++++++++++++-------------------------- 1 file changed, 62 insertions(+), 78 deletions(-) diff --git a/src/libs/util.ts b/src/libs/util.ts index f585bf2b..dfd3dde5 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -617,66 +617,61 @@ export const retrieveAllAccountsMetadata = async (): Promise< export const DUPLICATE_ACCOUNT_ERROR = "Cannot import duplicate account."; /** - * Saves a new identity to both SQL and Dexie databases + * Saves a new identity to SQL database */ export async function saveNewIdentity( identity: IIdentifier, mnemonic: string, derivationPath: string, ): Promise { - try { - // add to the new sql db - const platformService = await getPlatformService(); + // add to the new sql db + const platformService = await getPlatformService(); - // Check if account already exists before attempting to save - const existingAccount = await platformService.dbQuery( - "SELECT did FROM accounts WHERE did = ?", - [identity.did], - ); + // Check if account already exists before attempting to save + const existingAccount = await platformService.dbQuery( + "SELECT did FROM accounts WHERE did = ?", + [identity.did], + ); - if (existingAccount?.values?.length) { - throw new Error( - `Account with DID ${identity.did} already exists. ${DUPLICATE_ACCOUNT_ERROR}`, - ); - } + if (existingAccount?.values?.length) { + throw new Error( + `Account with DID ${identity.did} already exists. ${DUPLICATE_ACCOUNT_ERROR}`, + ); + } - const secrets = await platformService.dbQuery( - `SELECT secretBase64 FROM secret`, + const secrets = await platformService.dbQuery( + `SELECT secretBase64 FROM secret`, + ); + if (!secrets?.values?.length || !secrets.values[0]?.length) { + throw new Error( + "No initial encryption supported. We recommend you clear your data and start over.", ); - if (!secrets?.values?.length || !secrets.values[0]?.length) { - throw new Error( - "No initial encryption supported. We recommend you clear your data and start over.", - ); - } + } - const secretBase64 = secrets.values[0][0] as string; + const secretBase64 = secrets.values[0][0] as string; - const secret = base64ToArrayBuffer(secretBase64); - const identityStr = JSON.stringify(identity); - const encryptedIdentity = await simpleEncrypt(identityStr, secret); - const encryptedMnemonic = await simpleEncrypt(mnemonic, secret); - const encryptedIdentityBase64 = arrayBufferToBase64(encryptedIdentity); - const encryptedMnemonicBase64 = arrayBufferToBase64(encryptedMnemonic); + const secret = base64ToArrayBuffer(secretBase64); + const identityStr = JSON.stringify(identity); + const encryptedIdentity = await simpleEncrypt(identityStr, secret); + const encryptedMnemonic = await simpleEncrypt(mnemonic, secret); + const encryptedIdentityBase64 = arrayBufferToBase64(encryptedIdentity); + const encryptedMnemonicBase64 = arrayBufferToBase64(encryptedMnemonic); + + const sql = `INSERT INTO accounts (dateCreated, derivationPath, did, identityEncrBase64, mnemonicEncrBase64, publicKeyHex) + VALUES (?, ?, ?, ?, ?, ?)`; + const params = [ + new Date().toISOString(), + derivationPath, + identity.did, + encryptedIdentityBase64, + encryptedMnemonicBase64, + identity.keys[0].publicKeyHex, + ]; + await platformService.dbExec(sql, params); - const sql = `INSERT INTO accounts (dateCreated, derivationPath, did, identityEncrBase64, mnemonicEncrBase64, publicKeyHex) - VALUES (?, ?, ?, ?, ?, ?)`; - const params = [ - new Date().toISOString(), - derivationPath, - identity.did, - encryptedIdentityBase64, - encryptedMnemonicBase64, - identity.keys[0].publicKeyHex, - ]; - await platformService.dbExec(sql, params); - - await platformService.updateDefaultSettings({ activeDid: identity.did }); - - await platformService.insertNewDidIntoSettings(identity.did); - } catch (error) { - logger.error("Failed to save new identity:", error); - throw error; - } + await platformService.updateDefaultSettings({ activeDid: identity.did }); + + await platformService.insertNewDidIntoSettings(identity.did); } /** @@ -1074,39 +1069,28 @@ export async function checkForDuplicateAccount( didOrMnemonic: string, derivationPath?: string, ): Promise { - try { - let didToCheck: string; - - if (derivationPath) { - // Derive the DID from mnemonic and derivation path - const [address, privateHex, publicHex] = deriveAddress( - didOrMnemonic.trim().toLowerCase(), - derivationPath, - ); - - const newId = newIdentifier( - address, - privateHex, - publicHex, - derivationPath, - ); - didToCheck = newId.did; - } else { - // Use the provided DID directly - didToCheck = didOrMnemonic; - } + let didToCheck: string; - // Check if an account with this DID already exists - const platformService = await getPlatformService(); - const existingAccount = await platformService.dbQuery( - "SELECT did FROM accounts WHERE did = ?", - [didToCheck], + if (derivationPath) { + // Derive the DID from mnemonic and derivation path + const [address, privateHex, publicHex] = deriveAddress( + didOrMnemonic.trim().toLowerCase(), + derivationPath, ); - return (existingAccount?.values?.length ?? 0) > 0; - } catch (error) { - // If we can't check for duplicates, let the calling process handle the error - logger.error("Error checking for duplicate account:", error); - throw error; + const newId = newIdentifier(address, privateHex, publicHex, derivationPath); + didToCheck = newId.did; + } else { + // Use the provided DID directly + didToCheck = didOrMnemonic; } + + // Check if an account with this DID already exists + const platformService = await getPlatformService(); + const existingAccount = await platformService.dbQuery( + "SELECT did FROM accounts WHERE did = ?", + [didToCheck], + ); + + return (existingAccount?.values?.length ?? 0) > 0; } From ba587471f9080790ad9d223cb3878406e965c909 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Tue, 2 Sep 2025 19:11:50 -0600 Subject: [PATCH 20/20] doc: update the in-app help doc --- src/views/HelpView.vue | 42 +++++++++++++++++------------------------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/src/views/HelpView.vue b/src/views/HelpView.vue index 5604eae6..98bd7147 100644 --- a/src/views/HelpView.vue +++ b/src/views/HelpView.vue @@ -319,8 +319,9 @@
    • Go to Your Identity page, - click Advanced, and follow the instructions for the Contacts & Settings Database "Import". - Beware that this will erase your existing contact & settings. + click Advanced, and follow the instructions to "Import Contacts". + (There is currently no way to import other settings, so you'll have to recreate + by hand your search area, filters, etc.)
@@ -336,14 +337,18 @@

How do I erase my data from my device?

- Before doing this, you may want to back up your data with the instructions above. + Before doing this, you should back up your data with the instructions above. + Note that this does not erase data sent to our servers (see contact info below)