diff --git a/BUILDING.md b/BUILDING.md
index 27d6d678..1ac4ae9d 100644
--- a/BUILDING.md
+++ b/BUILDING.md
@@ -617,7 +617,8 @@ The Electron build process follows a multi-stage approach:
#### **Stage 2: Capacitor Sync**
- Copies web assets to Electron app directory
-- Syncs Capacitor configuration and plugins
+- Uses Electron-specific Capacitor configuration (not copied from main config)
+- Syncs Capacitor plugins for Electron platform
- Prepares native module bindings
#### **Stage 3: TypeScript Compile**
diff --git a/electron/capacitor.config.ts b/electron/capacitor.config.ts
new file mode 100644
index 00000000..24ef38c6
--- /dev/null
+++ b/electron/capacitor.config.ts
@@ -0,0 +1,116 @@
+import { CapacitorConfig } from '@capacitor/cli';
+
+const config: CapacitorConfig = {
+ appId: 'app.timesafari',
+ appName: 'TimeSafari',
+ webDir: 'dist',
+ server: {
+ cleartext: true
+ },
+ plugins: {
+ App: {
+ appUrlOpen: {
+ handlers: [
+ {
+ url: 'timesafari://*',
+ autoVerify: true
+ }
+ ]
+ }
+ },
+ SplashScreen: {
+ launchShowDuration: 3000,
+ launchAutoHide: true,
+ backgroundColor: '#ffffff',
+ androidSplashResourceName: 'splash',
+ androidScaleType: 'CENTER_CROP',
+ showSpinner: false,
+ androidSpinnerStyle: 'large',
+ iosSpinnerStyle: 'small',
+ spinnerColor: '#999999',
+ splashFullScreen: true,
+ splashImmersive: true
+ },
+ CapSQLite: {
+ iosDatabaseLocation: 'Library/CapacitorDatabase',
+ iosIsEncryption: false,
+ iosBiometric: {
+ biometricAuth: false,
+ biometricTitle: 'Biometric login for TimeSafari'
+ },
+ androidIsEncryption: false,
+ androidBiometric: {
+ biometricAuth: false,
+ biometricTitle: 'Biometric login for TimeSafari'
+ },
+ electronIsEncryption: false
+ }
+ },
+ ios: {
+ contentInset: 'never',
+ allowsLinkPreview: true,
+ scrollEnabled: true,
+ limitsNavigationsToAppBoundDomains: true,
+ backgroundColor: '#ffffff',
+ allowNavigation: [
+ '*.timesafari.app',
+ '*.jsdelivr.net',
+ 'api.endorser.ch'
+ ]
+ },
+ android: {
+ allowMixedContent: true,
+ captureInput: true,
+ webContentsDebuggingEnabled: false,
+ allowNavigation: [
+ '*.timesafari.app',
+ '*.jsdelivr.net',
+ 'api.endorser.ch',
+ '10.0.2.2:3000'
+ ]
+ },
+ electron: {
+ deepLinking: {
+ schemes: ['timesafari']
+ },
+ buildOptions: {
+ appId: 'app.timesafari',
+ productName: 'TimeSafari',
+ directories: {
+ output: 'dist-electron-packages'
+ },
+ files: [
+ 'dist/**/*',
+ 'electron/**/*'
+ ],
+ mac: {
+ category: 'public.app-category.productivity',
+ target: [
+ {
+ target: 'dmg',
+ arch: ['x64', 'arm64']
+ }
+ ]
+ },
+ win: {
+ target: [
+ {
+ target: 'nsis',
+ arch: ['x64']
+ }
+ ]
+ },
+ linux: {
+ target: [
+ {
+ target: 'AppImage',
+ arch: ['x64']
+ }
+ ],
+ category: 'Utility'
+ }
+ }
+ }
+};
+
+export default config;
diff --git a/electron/package-lock.json b/electron/package-lock.json
index 98a7fbdd..9cf915f4 100644
--- a/electron/package-lock.json
+++ b/electron/package-lock.json
@@ -56,7 +56,6 @@
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@capacitor-community/sqlite/-/sqlite-6.0.2.tgz",
"integrity": "sha512-sj+2SPLu7E/3dM3xxcWwfNomG+aQHuN96/EFGrOtp4Dv30/2y5oIPyi6hZGjQGjPc5GDNoTQwW7vxWNzybjuMg==",
- "license": "MIT",
"dependencies": {
"jeep-sqlite": "^2.7.2"
},
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;
diff --git a/electron/tsconfig.json b/electron/tsconfig.json
index b590aebb..d6057ede 100644
--- a/electron/tsconfig.json
+++ b/electron/tsconfig.json
@@ -1,6 +1,6 @@
{
"compileOnSave": true,
- "include": ["./src/**/*", "./capacitor.config.ts", "./capacitor.config.js"],
+ "include": ["./src/**/*"],
"compilerOptions": {
"outDir": "./build",
"importHelpers": true,
diff --git a/scripts/build-electron.sh b/scripts/build-electron.sh
index 7e7756b8..96213afa 100755
--- a/scripts/build-electron.sh
+++ b/scripts/build-electron.sh
@@ -181,7 +181,7 @@ sync_capacitor() {
copy_web_assets() {
log_info "Copying web assets to Electron"
safe_execute "Copying assets" "cp -r dist/* electron/app/"
- safe_execute "Copying config" "cp capacitor.config.json electron/capacitor.config.json"
+ # Note: Electron has its own capacitor.config.ts file, so we don't copy the main config
}
# Compile TypeScript
diff --git a/src/components/UserNameDialog.vue b/src/components/UserNameDialog.vue
index 285d11c2..aefc4e18 100644
--- a/src/components/UserNameDialog.vue
+++ b/src/components/UserNameDialog.vue
@@ -95,7 +95,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/constants/notifications.ts b/src/constants/notifications.ts
index 49b14812..5cc75bd4 100644
--- a/src/constants/notifications.ts
+++ b/src/constants/notifications.ts
@@ -1689,3 +1689,11 @@ export const NOTIFY_CONTACTS_ADDED_CONFIRM = {
title: "They're Added To Your List",
message: "Would you like to go to the main page now?",
};
+
+// ImportAccountView.vue specific constants
+// Used in: ImportAccountView.vue (onImportClick method - duplicate account warning)
+export const NOTIFY_DUPLICATE_ACCOUNT_IMPORT = {
+ title: "Account Already Imported",
+ message:
+ "This account has already been imported. Please use a different seed phrase or check your existing accounts.",
+};
diff --git a/src/libs/util.ts b/src/libs/util.ts
index d6418c05..38139803 100644
--- a/src/libs/util.ts
+++ b/src/libs/util.ts
@@ -614,58 +614,65 @@ 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
+ * 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();
- const secrets = await platformService.dbQuery(
- `SELECT secretBase64 FROM secret`,
+ // 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 (!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 secrets = await platformService.dbQuery(
+ `SELECT secretBase64 FROM secret`,
+ );
+ if (!secrets?.values?.length || !secrets.values[0]?.length) {
+ throw new Error(
+ "No initial encryption supported. We recommend you clear your data and start over.",
+ );
+ }
- const 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 secretBase64 = secrets.values[0][0] as string;
- 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 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);
// Update active identity in the active_identity table instead of settings
await platformService.updateActiveDid(identity.did);
- await platformService.insertNewDidIntoSettings(identity.did);
- } catch (error) {
- logger.error("Failed to update default settings:", error);
- throw new Error(
- "Failed to set default settings. Please try again or restart the app.",
- );
- }
+ await platformService.insertNewDidIntoSettings(identity.did);
}
/**
@@ -1034,3 +1041,58 @@ 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 {
+ 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;
+}
diff --git a/src/views/DIDView.vue b/src/views/DIDView.vue
index e2a41e77..4a793d0e 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)"
>
-
+
-
- Where do I get help with notifications?
-
-
- Here.
-
-
This app is misbehaving, like showing me a blank screen or failing to show my personal data.
What can I do?
@@ -434,10 +430,13 @@
- Drag down on the screen to refresh it; do that multiple times, because
+ For mobile apps, make sure you're connected to the internet.
+
+
+ For PWAs, drag down on the screen to refresh it; do that multiple times, because
it sometimes takes multiple tries for the app to refresh to the latest version.
You can see the version information at the bottom of this page; the best
- way to determine the latest version is to open this page in an incognito/private
+ way to determine the latest version is to open TimeSafari.app in an incognito/private
browser window and look at the version there.
@@ -468,9 +467,6 @@
Then reload Time Safari.
-
- Restart your device.
-
If you still have problems, you can clear the cache (see "erase my data" above)
@@ -508,16 +504,12 @@
- If using notifications, a server stores push token data. That can be revoked at any time
- by disabling notifications on the Profile page.
-
-
- If sending images, a server stores them, too. They can be removed by editing the claim
- and deleting them.
+ If sending images, a server stores them. They can be removed by editing each claim
+ and deleting the image.
If sending other partner system data (eg. to Trustroots) a public key and message
- data are stored on a server. Those can be removed via direct personal request.
+ data are stored on a server. Those can be removed via direct personal request (via contact below).
For all other claim data,
diff --git a/src/views/ImportAccountView.vue b/src/views/ImportAccountView.vue
index 97d1d22d..d4588423 100644
--- a/src/views/ImportAccountView.vue
+++ b/src/views/ImportAccountView.vue
@@ -88,9 +88,15 @@ import { Router } from "vue-router";
import { AppString, NotificationIface } from "../constants/app";
import { DEFAULT_ROOT_DERIVATION_PATH } from "../libs/crypto";
-import { retrieveAccountCount, importFromMnemonic } from "../libs/util";
+import {
+ retrieveAccountCount,
+ importFromMnemonic,
+ checkForDuplicateAccount,
+ DUPLICATE_ACCOUNT_ERROR,
+} from "../libs/util";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
+import { NOTIFY_DUPLICATE_ACCOUNT_IMPORT } from "@/constants/notifications";
/**
* Import Account View Component
@@ -198,6 +204,19 @@ export default class ImportAccountView extends Vue {
}
try {
+ // Check for duplicate account before importing
+ const isDuplicate = await checkForDuplicateAccount(
+ this.mnemonic,
+ this.derivationPath,
+ );
+ if (isDuplicate) {
+ this.notify.warning(
+ NOTIFY_DUPLICATE_ACCOUNT_IMPORT.message,
+ TIMEOUTS.LONG,
+ );
+ return;
+ }
+
await importFromMnemonic(
this.mnemonic,
this.derivationPath,
@@ -223,9 +242,20 @@ export default class ImportAccountView extends Vue {
this.$router.push({ name: "account" });
} catch (error: unknown) {
this.$logError("Import failed: " + error);
+
+ // Check if this is a duplicate account error from saveNewIdentity
+ const errorMessage =
+ error instanceof Error ? error.message : String(error);
+ if (errorMessage.includes(DUPLICATE_ACCOUNT_ERROR)) {
+ this.notify.warning(
+ NOTIFY_DUPLICATE_ACCOUNT_IMPORT.message,
+ TIMEOUTS.LONG,
+ );
+ return;
+ }
+
this.notify.error(
- (error instanceof Error ? error.message : String(error)) ||
- "Failed to import account.",
+ errorMessage || "Failed to import account.",
TIMEOUTS.LONG,
);
}
diff --git a/src/views/ImportDerivedAccountView.vue b/src/views/ImportDerivedAccountView.vue
index 9127326b..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";
@@ -171,6 +172,16 @@ export default class ImportAccountView extends Vue {
const newId = newIdentifier(address, publicHex, privateHex, newDerivPath);
try {
+ // Check for duplicate account before creating
+ const isDuplicate = await checkForDuplicateAccount(newId.did);
+ if (isDuplicate) {
+ this.notify.warning(
+ "This derived account already exists. Please try a different derivation path.",
+ TIMEOUTS.LONG,
+ );
+ return;
+ }
+
await saveNewIdentity(newId, mne, newDerivPath);
// record that as the active DID
diff --git a/src/views/NewEditAccountView.vue b/src/views/NewEditAccountView.vue
index 78e709f2..d1349e5a 100644
--- a/src/views/NewEditAccountView.vue
+++ b/src/views/NewEditAccountView.vue
@@ -110,10 +110,22 @@ 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,
+ });
+ } else {
+ // Fallback to master settings if no active DID
+ await this.$saveSettings({
+ firstName: this.givenName,
+ });
+ }
+
this.$router.back();
}
diff --git a/src/views/QuickActionBvcEndView.vue b/src/views/QuickActionBvcEndView.vue
index 8c4126fa..6db87141 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() {
@@ -296,6 +305,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) {
diff --git a/test-playwright/00-noid-tests.spec.ts b/test-playwright/00-noid-tests.spec.ts
index d3c848d4..f3a323fc 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, importUserFromAccount } from './testUtils';
import { NOTIFY_CONTACT_INVALID_DID } from '../src/constants/notifications';
test('Check activity feed - check that server is running', async ({ page }) => {
diff --git a/test-playwright/03-duplicate-import-test.spec.ts b/test-playwright/03-duplicate-import-test.spec.ts
new file mode 100644
index 00000000..0bdc80bd
--- /dev/null
+++ b/test-playwright/03-duplicate-import-test.spec.ts
@@ -0,0 +1,63 @@
+import { test, expect } from '@playwright/test';
+import { importUserFromAccount, getTestUserData } from './testUtils';
+import { NOTIFY_DUPLICATE_ACCOUNT_IMPORT } from '../src/constants/notifications';
+
+/**
+ * Test duplicate account import functionality
+ *
+ * This test verifies that:
+ * 1. A user can successfully import an account the first time
+ * 2. Attempting to import the same account again shows a warning message
+ * 3. The duplicate import is prevented
+ */
+test.describe('Duplicate Account Import', () => {
+ test('should prevent importing the same account twice', async ({ page }) => {
+ const userData = getTestUserData("00");
+
+ // First import - should succeed
+ await page.goto("./start");
+ await page.getByText("You have a seed").click();
+ await page.getByPlaceholder("Seed Phrase").fill(userData.seedPhrase);
+ await page.getByRole("button", { name: "Import" }).click();
+
+ // Verify first import was successful
+ await expect(page.getByRole("code")).toContainText(userData.did);
+
+ // Navigate back to start page for second import attempt
+ await page.goto("./start");
+ await page.getByText("You have a seed").click();
+ await page.getByPlaceholder("Seed Phrase").fill(userData.seedPhrase);
+ await page.getByRole("button", { name: "Import" }).click();
+
+ // 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(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();
+ });
+
+ test('should allow importing different accounts', async ({ page }) => {
+ const userZeroData = getTestUserData("00");
+ const userOneData = getTestUserData("01");
+
+ // Import first user
+ await page.goto("./start");
+ await page.getByText("You have a seed").click();
+ await page.getByPlaceholder("Seed Phrase").fill(userZeroData.seedPhrase);
+ await page.getByRole("button", { name: "Import" }).click();
+
+ // Verify first import was successful
+ await expect(page.getByRole("code")).toContainText(userZeroData.did);
+
+ // Navigate back to start page for second user import
+ await page.goto("./start");
+ await page.getByText("You have a seed").click();
+ await page.getByPlaceholder("Seed Phrase").fill(userOneData.seedPhrase);
+ await page.getByRole("button", { name: "Import" }).click();
+
+ // Verify second import was successful (should not show duplicate warning)
+ await expect(page.getByRole("code")).toContainText(userOneData.did);
+ await expect(page.getByText(NOTIFY_DUPLICATE_ACCOUNT_IMPORT.message)).not.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()