Compare commits
17 Commits
profile_in
...
account-im
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c7cb9333e | |||
| fa8956fb38 | |||
|
|
1499211018 | ||
|
|
25e37cc415 | ||
|
|
d339f1a274 | ||
|
|
c2e7531554 | ||
| 2b9b43d08f | |||
|
|
5f8d1fc8c6 | ||
|
|
c9082fa57b | ||
| a7608429be | |||
|
|
dde37e73e1 | ||
|
|
83c0c18db2 | ||
|
|
5780d96cdc | ||
|
|
96e4d3c394 | ||
|
|
c4f2bb5e3a | ||
|
|
f51408e32a | ||
|
|
8827c4a973 |
@@ -617,7 +617,8 @@ The Electron build process follows a multi-stage approach:
|
|||||||
#### **Stage 2: Capacitor Sync**
|
#### **Stage 2: Capacitor Sync**
|
||||||
|
|
||||||
- Copies web assets to Electron app directory
|
- 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
|
- Prepares native module bindings
|
||||||
|
|
||||||
#### **Stage 3: TypeScript Compile**
|
#### **Stage 3: TypeScript Compile**
|
||||||
|
|||||||
116
electron/capacitor.config.ts
Normal file
116
electron/capacitor.config.ts
Normal file
@@ -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;
|
||||||
1
electron/package-lock.json
generated
1
electron/package-lock.json
generated
@@ -56,7 +56,6 @@
|
|||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@capacitor-community/sqlite/-/sqlite-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@capacitor-community/sqlite/-/sqlite-6.0.2.tgz",
|
||||||
"integrity": "sha512-sj+2SPLu7E/3dM3xxcWwfNomG+aQHuN96/EFGrOtp4Dv30/2y5oIPyi6hZGjQGjPc5GDNoTQwW7vxWNzybjuMg==",
|
"integrity": "sha512-sj+2SPLu7E/3dM3xxcWwfNomG+aQHuN96/EFGrOtp4Dv30/2y5oIPyi6hZGjQGjPc5GDNoTQwW7vxWNzybjuMg==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jeep-sqlite": "^2.7.2"
|
"jeep-sqlite": "^2.7.2"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compileOnSave": true,
|
"compileOnSave": true,
|
||||||
"include": ["./src/**/*", "./capacitor.config.ts", "./capacitor.config.js"],
|
"include": ["./src/**/*"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "./build",
|
"outDir": "./build",
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ sync_capacitor() {
|
|||||||
copy_web_assets() {
|
copy_web_assets() {
|
||||||
log_info "Copying web assets to Electron"
|
log_info "Copying web assets to Electron"
|
||||||
safe_execute "Copying assets" "cp -r dist/* electron/app/"
|
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
|
# Compile TypeScript
|
||||||
|
|||||||
@@ -84,7 +84,8 @@ export default class UserNameDialog extends Vue {
|
|||||||
*/
|
*/
|
||||||
async open(aCallback?: (name?: string) => void) {
|
async open(aCallback?: (name?: string) => void) {
|
||||||
this.callback = aCallback || this.callback;
|
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.givenName = settings.firstName || "";
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
}
|
}
|
||||||
@@ -95,7 +96,18 @@ export default class UserNameDialog extends Vue {
|
|||||||
*/
|
*/
|
||||||
async onClickSaveChanges() {
|
async onClickSaveChanges() {
|
||||||
try {
|
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.visible = false;
|
||||||
this.callback(this.givenName);
|
this.callback(this.givenName);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1689,3 +1689,11 @@ export const NOTIFY_CONTACTS_ADDED_CONFIRM = {
|
|||||||
title: "They're Added To Your List",
|
title: "They're Added To Your List",
|
||||||
message: "Would you like to go to the main page now?",
|
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.",
|
||||||
|
};
|
||||||
|
|||||||
140
src/libs/util.ts
140
src/libs/util.ts
@@ -614,57 +614,64 @@ export const retrieveAllAccountsMetadata = async (): Promise<
|
|||||||
return result;
|
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(
|
export async function saveNewIdentity(
|
||||||
identity: IIdentifier,
|
identity: IIdentifier,
|
||||||
mnemonic: string,
|
mnemonic: string,
|
||||||
derivationPath: string,
|
derivationPath: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
// add to the new sql db
|
||||||
// add to the new sql db
|
const platformService = await getPlatformService();
|
||||||
const platformService = await getPlatformService();
|
|
||||||
|
|
||||||
const secrets = await platformService.dbQuery(
|
// Check if account already exists before attempting to save
|
||||||
`SELECT secretBase64 FROM secret`,
|
const existingAccount = await platformService.dbQuery(
|
||||||
);
|
"SELECT did FROM accounts WHERE did = ?",
|
||||||
if (!secrets?.values?.length || !secrets.values[0]?.length) {
|
[identity.did],
|
||||||
throw new Error(
|
);
|
||||||
"No initial encryption supported. We recommend you clear your data and start over.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const secretBase64 = secrets.values[0][0] as string;
|
if (existingAccount?.values?.length) {
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
await platformService.updateDefaultSettings({ activeDid: identity.did });
|
|
||||||
|
|
||||||
await platformService.insertNewDidIntoSettings(identity.did);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Failed to update default settings:", error);
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Failed to set default settings. Please try again or restart the app.",
|
`Account with DID ${identity.did} already exists. ${DUPLICATE_ACCOUNT_ERROR}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 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 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1032,3 +1039,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<boolean> - True if account already exists, false otherwise
|
||||||
|
* @throws Error if database query fails
|
||||||
|
*/
|
||||||
|
export async function checkForDuplicateAccount(did: string): Promise<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<boolean> - True if account already exists, false otherwise
|
||||||
|
* @throws Error if database query fails
|
||||||
|
*/
|
||||||
|
export async function checkForDuplicateAccount(
|
||||||
|
mnemonic: string,
|
||||||
|
derivationPath: string,
|
||||||
|
): Promise<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of checkForDuplicateAccount with overloaded signatures
|
||||||
|
*/
|
||||||
|
export async function checkForDuplicateAccount(
|
||||||
|
didOrMnemonic: string,
|
||||||
|
derivationPath?: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -88,9 +88,15 @@ import { Router } from "vue-router";
|
|||||||
|
|
||||||
import { AppString, NotificationIface } from "../constants/app";
|
import { AppString, NotificationIface } from "../constants/app";
|
||||||
import { DEFAULT_ROOT_DERIVATION_PATH } from "../libs/crypto";
|
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 { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||||
|
import { NOTIFY_DUPLICATE_ACCOUNT_IMPORT } from "@/constants/notifications";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Import Account View Component
|
* Import Account View Component
|
||||||
@@ -198,6 +204,19 @@ export default class ImportAccountView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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(
|
await importFromMnemonic(
|
||||||
this.mnemonic,
|
this.mnemonic,
|
||||||
this.derivationPath,
|
this.derivationPath,
|
||||||
@@ -223,9 +242,20 @@ export default class ImportAccountView extends Vue {
|
|||||||
this.$router.push({ name: "account" });
|
this.$router.push({ name: "account" });
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
this.$logError("Import failed: " + error);
|
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(
|
this.notify.error(
|
||||||
(error instanceof Error ? error.message : String(error)) ||
|
errorMessage || "Failed to import account.",
|
||||||
"Failed to import account.",
|
|
||||||
TIMEOUTS.LONG,
|
TIMEOUTS.LONG,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ import {
|
|||||||
retrieveAllAccountsMetadata,
|
retrieveAllAccountsMetadata,
|
||||||
retrieveFullyDecryptedAccount,
|
retrieveFullyDecryptedAccount,
|
||||||
saveNewIdentity,
|
saveNewIdentity,
|
||||||
|
checkForDuplicateAccount,
|
||||||
} from "../libs/util";
|
} from "../libs/util";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { Account, AccountEncrypted } from "../db/tables/accounts";
|
import { Account, AccountEncrypted } from "../db/tables/accounts";
|
||||||
@@ -171,6 +172,16 @@ export default class ImportAccountView extends Vue {
|
|||||||
const newId = newIdentifier(address, publicHex, privateHex, newDerivPath);
|
const newId = newIdentifier(address, publicHex, privateHex, newDerivPath);
|
||||||
|
|
||||||
try {
|
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);
|
await saveNewIdentity(newId, mne, newDerivPath);
|
||||||
|
|
||||||
// record that as the active DID
|
// record that as the active DID
|
||||||
|
|||||||
@@ -110,10 +110,22 @@ export default class NewEditAccountView extends Vue {
|
|||||||
* @async
|
* @async
|
||||||
*/
|
*/
|
||||||
async onClickSaveChanges() {
|
async onClickSaveChanges() {
|
||||||
await this.$updateSettings({
|
// Get the current active DID to save to user-specific settings
|
||||||
firstName: this.givenName,
|
const settings = await this.$accountSettings();
|
||||||
lastName: "", // deprecated, pre v 0.1.3
|
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();
|
this.$router.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -243,13 +243,19 @@
|
|||||||
:project-name="name"
|
:project-name="name"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<h3 class="text-lg font-bold leading-tight mb-3">Offered To This Idea</h3>
|
<h3 class="text-lg font-bold leading-tight mb-3">
|
||||||
|
Offered To This Idea
|
||||||
|
</h3>
|
||||||
|
|
||||||
<div v-if="offersToThis.length === 0" class="text-sm">
|
<div v-if="offersToThis.length === 0" class="text-sm">
|
||||||
(None yet.<span v-if="activeDid && isRegistered"> Wanna
|
(None yet.<span v-if="activeDid && isRegistered">
|
||||||
<span class="cursor-pointer text-blue-500" @click="openOfferDialog()"
|
Wanna
|
||||||
>offer something… especially if others join you</span
|
<span
|
||||||
>?</span>)
|
class="cursor-pointer text-blue-500"
|
||||||
|
@click="openOfferDialog()"
|
||||||
|
>offer something… especially if others join you</span
|
||||||
|
>?</span
|
||||||
|
>)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul v-else class="text-sm border-t border-slate-300">
|
<ul v-else class="text-sm border-t border-slate-300">
|
||||||
@@ -325,7 +331,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="text-lg font-bold leading-tight mb-3">Given To This Project</h3>
|
<h3 class="text-lg font-bold leading-tight mb-3">
|
||||||
|
Given To This Project
|
||||||
|
</h3>
|
||||||
|
|
||||||
<div v-if="givesToThis.length === 0" class="text-sm">
|
<div v-if="givesToThis.length === 0" class="text-sm">
|
||||||
(None yet. If you've seen something, say something by clicking a
|
(None yet. If you've seen something, say something by clicking a
|
||||||
@@ -498,7 +506,9 @@
|
|||||||
Benefitted From This Project
|
Benefitted From This Project
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div v-if="givesProvidedByThis.length === 0" class="text-sm">(None yet.)</div>
|
<div v-if="givesProvidedByThis.length === 0" class="text-sm">
|
||||||
|
(None yet.)
|
||||||
|
</div>
|
||||||
|
|
||||||
<ul v-else class="text-sm border-t border-slate-300">
|
<ul v-else class="text-sm border-t border-slate-300">
|
||||||
<li
|
<li
|
||||||
|
|||||||
63
test-playwright/03-duplicate-import-test.spec.ts
Normal file
63
test-playwright/03-duplicate-import-test.spec.ts
Normal file
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user