+
+
+
{account.name}
+
+
{account.watchOnly && (
@@ -93,18 +95,18 @@ const AccountListItem = ({
)}
-
-
- {truncateString(account.identity, { leading: 5, trailing: 5 })}
-
-
- {isVisible
- ? balance !== undefined
- ? formatBalanceCompact(balance)
- : '--'
- : HIDDEN_BALANCE}
-
-
+
+
+
+ {truncateString(account.identity, { leading: 5, trailing: 5 })}
+
+
+ {isVisible
+ ? balance !== undefined
+ ? formatBalanceCompact(balance)
+ : '--'
+ : HIDDEN_BALANCE}
+
diff --git a/src/components/ui/input-group.tsx b/src/components/ui/input-group.tsx
index a9666b6..a1d66fe 100644
--- a/src/components/ui/input-group.tsx
+++ b/src/components/ui/input-group.tsx
@@ -13,11 +13,11 @@ function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
role="group"
className={cn(
'group/input-group border-input bg-muted/30 relative flex w-full items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none',
- 'h-9 min-w-0 has-[>textarea]:h-auto',
+ 'h-12 min-w-0 has-[>textarea]:h-auto',
// Variants based on alignment.
- 'has-[>[data-align=inline-start]]:[&>input]:pl-2',
- 'has-[>[data-align=inline-end]]:[&>input]:pr-2',
+ 'has-[>[data-align=inline-start]]:[&>input]:pl-3',
+ 'has-[>[data-align=inline-end]]:[&>input]:pr-3',
'has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3',
'has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3',
diff --git a/src/lib/dapp/controller.ts b/src/lib/dapp/controller.ts
index aaefcc8..3638ded 100644
--- a/src/lib/dapp/controller.ts
+++ b/src/lib/dapp/controller.ts
@@ -430,8 +430,8 @@ const executeApprovedRequest = async (
if (!decision.approved) {
return asDappFailure(request.id, 'USER_REJECTED', 'Request was rejected by user')
}
- const passphrase = decision.passphrase?.trim()
- if (!passphrase) {
+ const passphrase = decision.passphrase
+ if (!passphrase || !passphrase.trim()) {
return asDappFailure(request.id, 'INVALID_PASSPHRASE', 'Passphrase is required')
}
const permissions = await getDappPermissions()
diff --git a/src/lib/lock.ts b/src/lib/lock.ts
index f9de55a..de25f4f 100644
--- a/src/lib/lock.ts
+++ b/src/lib/lock.ts
@@ -8,6 +8,7 @@ const MIN_LOCK_TIMEOUT_MINUTES = 0
const MAX_LOCK_TIMEOUT_MINUTES = 120
const ACTIVITY_TOUCH_THROTTLE_MS = 1_000
let lastActivityTouchAt = 0
+let sessionUnlockedMemory = false
type ChromeStorageSession = {
get: (keys: string[], callback: (items: Record
) => void) => void
@@ -28,6 +29,7 @@ const getChromeSessionStorage = (): ChromeStorageSession | null => {
}
const setSessionUnlockedFlag = (value: boolean) => {
+ sessionUnlockedMemory = value
const sessionStorage = getChromeSessionStorage()
if (!sessionStorage) return
@@ -155,6 +157,8 @@ const normalizeLockTimeoutOption = (minutes: number) => {
}
const readSessionUnlockedFlag = async () => {
+ if (sessionUnlockedMemory) return true
+
const sessionStorage = getChromeSessionStorage()
if (!sessionStorage) return true
diff --git a/src/lib/passphrase.ts b/src/lib/passphrase.ts
new file mode 100644
index 0000000..9a06dfc
--- /dev/null
+++ b/src/lib/passphrase.ts
@@ -0,0 +1,18 @@
+const MIN_PASSPHRASE_LENGTH = 12
+
+export type PassphraseStrengthResult =
+ | { valid: true }
+ | { valid: false; reason: 'required' | 'tooShort' }
+
+export const validatePassphraseStrength = (
+ passphrase: string,
+ options: { requireMinLength?: boolean } = {},
+): PassphraseStrengthResult => {
+ const trimmed = passphrase.trim()
+ if (!trimmed) return { valid: false, reason: 'required' }
+ const requireMinLength = options.requireMinLength ?? true
+ if (requireMinLength && trimmed.length < MIN_PASSPHRASE_LENGTH) {
+ return { valid: false, reason: 'tooShort' }
+ }
+ return { valid: true }
+}
diff --git a/src/lib/vault-password.ts b/src/lib/vault-password.ts
index 092ad99..7f5c99f 100644
--- a/src/lib/vault-password.ts
+++ b/src/lib/vault-password.ts
@@ -4,9 +4,7 @@ export const changeVaultPassphrase = async (
currentPassphrase: string,
newPassphrase: string,
): Promise => {
- const trimmedCurrent = currentPassphrase.trim()
- const trimmedNew = newPassphrase.trim()
- const vault = await openBrowserVault(trimmedCurrent, false)
- await vault.rotatePassphrase(trimmedNew)
+ const vault = await openBrowserVault(currentPassphrase, false)
+ await vault.rotatePassphrase(newPassphrase)
await vault.save()
}
diff --git a/src/locales/de.json b/src/locales/de.json
index d0dccba..9490870 100644
--- a/src/locales/de.json
+++ b/src/locales/de.json
@@ -585,7 +585,7 @@
"fileTooLarge": "Die ausgewählte Datei ist zu groß für eine gültige Tresor-Datei.",
"selectFile": "Wähle eine Tresor-Datei aus.",
"passphraseRequired": "Passwort ist erforderlich.",
- "importFailed": "Web-Wallet-Tresor konnte nicht importiert werden.",
+ "invalidSourcePassphrase": "Ungültiges Quell-Tresor-Passwort.",
"noEntries": "Tresor importiert, aber keine Einträge gefunden.",
"generic": "Tresor konnte nicht importiert werden.",
"invalidFileOrPassphrase": "Die ausgewählte Datei ist keine Qubic-Tresor-Datei oder das Passwort ist falsch."
diff --git a/src/locales/en.json b/src/locales/en.json
index f431abb..be160e0 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -585,7 +585,7 @@
"fileTooLarge": "The selected file is too large to be a valid vault file.",
"selectFile": "Select a vault file.",
"passphraseRequired": "Password is required.",
- "importFailed": "Failed to import web wallet vault.",
+ "invalidSourcePassphrase": "Invalid source vault password.",
"noEntries": "Vault imported but no entries were found.",
"generic": "Failed to import vault.",
"invalidFileOrPassphrase": "Selected file is not a Qubic vault file or password is incorrect."
diff --git a/src/locales/es.json b/src/locales/es.json
index ea7038a..2943b03 100644
--- a/src/locales/es.json
+++ b/src/locales/es.json
@@ -585,7 +585,7 @@
"fileTooLarge": "El archivo seleccionado es demasiado grande para ser un vault válido.",
"selectFile": "Selecciona un archivo vault.",
"passphraseRequired": "Se requiere contraseña.",
- "importFailed": "No se pudo importar el vault de la billetera web.",
+ "invalidSourcePassphrase": "Contraseña del vault de origen inválida.",
"noEntries": "Vault importado pero no se encontraron entradas.",
"generic": "No se pudo importar el vault.",
"invalidFileOrPassphrase": "El archivo seleccionado no es un archivo Qubic vault o la contraseña es incorrecta."
diff --git a/src/locales/fr.json b/src/locales/fr.json
index 50fef7d..511f9d5 100644
--- a/src/locales/fr.json
+++ b/src/locales/fr.json
@@ -585,7 +585,7 @@
"fileTooLarge": "Le fichier sélectionné est trop volumineux pour être un fichier vault valide.",
"selectFile": "Sélectionnez un fichier vault.",
"passphraseRequired": "Le mot de passe est requis.",
- "importFailed": "Échec de l'importation du vault du portefeuille web.",
+ "invalidSourcePassphrase": "Mot de passe du vault source invalide.",
"noEntries": "Vault importé mais aucune entrée trouvée.",
"generic": "Échec de l'importation du vault.",
"invalidFileOrPassphrase": "Le fichier sélectionné n'est pas un fichier Qubic vault ou le mot de passe est incorrect."
diff --git a/src/locales/ru.json b/src/locales/ru.json
index daec6aa..0a7d59d 100644
--- a/src/locales/ru.json
+++ b/src/locales/ru.json
@@ -585,7 +585,7 @@
"fileTooLarge": "Выбранный файл слишком большой для допустимого файла vault.",
"selectFile": "Выберите файл vault.",
"passphraseRequired": "Необходимо ввести пароль.",
- "importFailed": "Не удалось импортировать vault веб-кошелька.",
+ "invalidSourcePassphrase": "Неверный пароль исходного vault.",
"noEntries": "Vault импортирован, но записи не найдены.",
"generic": "Не удалось импортировать vault.",
"invalidFileOrPassphrase": "Выбранный файл не является файлом Qubic vault или пароль введён неверно."
diff --git a/src/locales/tr.json b/src/locales/tr.json
index f0efcf4..0be0943 100644
--- a/src/locales/tr.json
+++ b/src/locales/tr.json
@@ -585,7 +585,7 @@
"fileTooLarge": "Seçilen dosya geçerli bir vault dosyası için çok büyük.",
"selectFile": "Bir vault dosyası seçin.",
"passphraseRequired": "Parola gereklidir.",
- "importFailed": "Web cüzdanı vault dosyası içe aktarılamadı.",
+ "invalidSourcePassphrase": "Kaynak vault parolası geçersiz.",
"noEntries": "Vault içe aktarıldı ancak hiçbir giriş bulunamadı.",
"generic": "Vault içe aktarılamadı.",
"invalidFileOrPassphrase": "Seçilen dosya bir Qubic vault dosyası değil veya parola yanlış."
diff --git a/src/locales/vi.json b/src/locales/vi.json
index cc4e6fe..da78d45 100644
--- a/src/locales/vi.json
+++ b/src/locales/vi.json
@@ -585,7 +585,7 @@
"fileTooLarge": "Tập tin đã chọn quá lớn để là tập tin vault hợp lệ.",
"selectFile": "Chọn một tập tin vault.",
"passphraseRequired": "Yêu cầu mật khẩu.",
- "importFailed": "Không thể nhập vault từ ví web.",
+ "invalidSourcePassphrase": "Mật khẩu vault nguồn không hợp lệ.",
"noEntries": "Đã nhập vault nhưng không tìm thấy mục nào.",
"generic": "Không thể nhập vault.",
"invalidFileOrPassphrase": "Tập tin đã chọn không phải là tập tin Qubic vault hoặc mật khẩu không chính xác."
diff --git a/src/locales/zh.json b/src/locales/zh.json
index 1c551ca..615d179 100644
--- a/src/locales/zh.json
+++ b/src/locales/zh.json
@@ -585,7 +585,7 @@
"fileTooLarge": "所选文件过大,不是有效的 vault 文件。",
"selectFile": "请选择 vault 文件。",
"passphraseRequired": "请输入密码。",
- "importFailed": "导入网页钱包 vault 失败。",
+ "invalidSourcePassphrase": "来源 vault 密码无效。",
"noEntries": "vault 已导入但未找到任何条目。",
"generic": "导入 vault 失败。",
"invalidFileOrPassphrase": "选择的文件不是 Qubic vault 文件,或者密码不正确。"
diff --git a/src/pages/manage-accounts.tsx b/src/pages/manage-accounts.tsx
index 28a8544..823c6ba 100644
--- a/src/pages/manage-accounts.tsx
+++ b/src/pages/manage-accounts.tsx
@@ -336,9 +336,8 @@ const ManageAccounts = () => {
setPassphraseError(t('accounts.manage.errors.passphraseRequired'))
return
}
- const passphrase = passphraseInput.trim()
try {
- const vault = await openBrowserVault(passphrase, false)
+ const vault = await openBrowserVault(passphraseInput, false)
const expectedIdentity = vault.list()[0]?.identity
if (expectedIdentity) {
await vault.getSeed(expectedIdentity)
@@ -350,19 +349,19 @@ const ManageAccounts = () => {
const action = pendingAction
setPendingAction(null)
if (action.type === 'load') {
- await loadAccounts(passphrase)
+ await loadAccounts(passphraseInput)
return
}
if (action.type === 'remove') {
- await handleRemove(action.account, passphrase)
+ await handleRemove(action.account, passphraseInput)
return
}
if (action.type === 'reveal') {
- await handleRevealSeed(action.account, passphrase)
+ await handleRevealSeed(action.account, passphraseInput)
return
}
if (action.type === 'rename') {
- await handleRename(action.account, action.name, passphrase)
+ await handleRename(action.account, action.name, passphraseInput)
}
} catch (error) {
if (
diff --git a/src/pages/onboarding/create-wallet.tsx b/src/pages/onboarding/create-wallet.tsx
index 42af644..e19f06a 100644
--- a/src/pages/onboarding/create-wallet.tsx
+++ b/src/pages/onboarding/create-wallet.tsx
@@ -5,6 +5,7 @@ import { useNavigate } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { Button } from '@/components/ui/button'
import { generateSeed, isSeedLike } from '@/lib/seed'
+import { validatePassphraseStrength } from '@/lib/passphrase'
import { setUnlocked } from '@/lib/lock'
import {
openBrowserVault,
@@ -112,9 +113,20 @@ const CreateWallet = ({
}
}
- if (step === 2 && !passphrase.trim()) {
- setStatus(t('onboarding.errors.passphraseRequired'))
- return
+ if (step === 2) {
+ const validation = validatePassphraseStrength(passphrase, {
+ requireMinLength: variant !== 'add-address',
+ })
+ if (!validation.valid) {
+ setStatus(
+ t(
+ validation.reason === 'required'
+ ? 'onboarding.errors.passphraseRequired'
+ : 'onboarding.errors.passphraseTooShort',
+ ),
+ )
+ return
+ }
}
if (step === 2 && variant !== 'add-address') {
if (!confirmPassphrase.trim()) {
@@ -125,17 +137,13 @@ const CreateWallet = ({
setStatus(t('onboarding.errors.passphraseMismatch'))
return
}
- if (passphrase.length < 12) {
- setStatus(t('onboarding.errors.passphraseTooShort'))
- return
- }
}
if (step === 2 && variant === 'add-address') {
if (isAccountNameTaken(name)) {
setStatus(t('accounts.manage.errors.nameDuplicate'))
return
}
- const result = await validateVaultPassphrase(passphrase.trim())
+ const result = await validateVaultPassphrase(passphrase)
if (!result.valid) {
setStatus(
result.reason === 'invalid'
@@ -168,8 +176,17 @@ const CreateWallet = ({
return
}
- if (!passphrase.trim()) {
- setStatus(t('onboarding.errors.passphraseRequired'))
+ const validation = validatePassphraseStrength(passphrase, {
+ requireMinLength: variant !== 'add-address',
+ })
+ if (!validation.valid) {
+ setStatus(
+ t(
+ validation.reason === 'required'
+ ? 'onboarding.errors.passphraseRequired'
+ : 'onboarding.errors.passphraseTooShort',
+ ),
+ )
setStep(2)
return
}
@@ -184,11 +201,6 @@ const CreateWallet = ({
setStep(2)
return
}
- if (passphrase.length < 12) {
- setStatus(t('onboarding.errors.passphraseTooShort'))
- setStep(2)
- return
- }
}
const cachedAccounts = getCachedAccounts()
diff --git a/src/pages/onboarding/import-seed.tsx b/src/pages/onboarding/import-seed.tsx
index db18362..6cd413b 100644
--- a/src/pages/onboarding/import-seed.tsx
+++ b/src/pages/onboarding/import-seed.tsx
@@ -9,6 +9,7 @@ import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { PasswordInput } from '@/components/ui/password-input'
import { isSeedLike } from '@/lib/seed'
+import { validatePassphraseStrength } from '@/lib/passphrase'
import {
getCachedAccounts,
getSuggestedNextAccountName,
@@ -102,16 +103,27 @@ const ImportSeed = ({
}
}
- if (step === 2 && !passphrase.trim()) {
- setStatus(t('onboarding.errors.passphraseRequired'))
- return
+ if (step === 2) {
+ const validation = validatePassphraseStrength(passphrase, {
+ requireMinLength: variant !== 'add-address',
+ })
+ if (!validation.valid) {
+ setStatus(
+ t(
+ validation.reason === 'required'
+ ? 'onboarding.errors.passphraseRequired'
+ : 'onboarding.errors.passphraseTooShort',
+ ),
+ )
+ return
+ }
}
if (step === 2 && variant === 'add-address') {
if (isAccountNameTaken(name)) {
setStatus(t('accounts.manage.errors.nameDuplicate'))
return
}
- const result = await validateVaultPassphrase(passphrase.trim())
+ const result = await validateVaultPassphrase(passphrase)
if (!result.valid) {
setStatus(
result.reason === 'invalid'
@@ -144,8 +156,17 @@ const ImportSeed = ({
return
}
- if (!passphrase.trim()) {
- setStatus(t('onboarding.errors.passphraseRequired'))
+ const validation = validatePassphraseStrength(passphrase, {
+ requireMinLength: variant !== 'add-address',
+ })
+ if (!validation.valid) {
+ setStatus(
+ t(
+ validation.reason === 'required'
+ ? 'onboarding.errors.passphraseRequired'
+ : 'onboarding.errors.passphraseTooShort',
+ ),
+ )
setStep(2)
return
}
diff --git a/src/pages/onboarding/import-vault.tsx b/src/pages/onboarding/import-vault.tsx
index 14b641a..2edddcd 100644
--- a/src/pages/onboarding/import-vault.tsx
+++ b/src/pages/onboarding/import-vault.tsx
@@ -1,4 +1,5 @@
import { useMemo, useState } from 'react'
+import { VaultInvalidPassphraseError } from '@qubic-labs/sdk'
import { ArrowLeftIcon, ArrowRightIcon, FileJsonIcon, UploadCloudIcon } from 'lucide-react'
import { useNavigate } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
@@ -16,6 +17,15 @@ import FlowHeader from '@/components/onboarding/flow-header'
const TOTAL_STEPS = 2
const MAX_VAULT_FILE_SIZE = 102_400
+const isWebWalletVaultFile = (fileText: string) => {
+ try {
+ const data = JSON.parse(fileText)
+ return !!(data.salt && data.iv && data.cipher)
+ } catch {
+ return false
+ }
+}
+
const ImportVault = () => {
const navigate = useNavigate()
const { t } = useTranslation()
@@ -89,26 +99,14 @@ const ImportVault = () => {
setIsSaving(true)
const fileText = await file.text()
- let isWebWalletVault = false
- try {
- const data = JSON.parse(fileText)
- isWebWalletVault = !!(data.salt && data.iv && data.cipher)
- } catch {
- // Not valid JSON — treat as SDK format
- }
+ const isWebWalletVault = isWebWalletVaultFile(fileText)
if (isWebWalletVault) {
const qubicVault = new QubicVault()
let unlockSucceeded = false
try {
- unlockSucceeded = await qubicVault.importAndUnlock(
- true,
- passphrase.trim(),
- null,
- file,
- false,
- )
+ unlockSucceeded = await qubicVault.importAndUnlock(true, passphrase, null, file, false)
} catch {
// SDK rejects with a string on wrong password — fall through to the check below
}
@@ -132,7 +130,7 @@ const ImportVault = () => {
return
}
- const vault = await openBrowserVault(passphrase.trim(), true)
+ const vault = await openBrowserVault(passphrase, true)
const watchOnlyAccounts = getWatchOnlyAccounts()
for (const seed of seeds) {
@@ -161,11 +159,11 @@ const ImportVault = () => {
clearSensitiveState()
navigate('/home')
} else {
- const vault = await openBrowserVault(passphrase.trim(), true)
+ const vault = await openBrowserVault(passphrase, true)
try {
await vault.importEncrypted(fileText, {
mode: 'merge',
- sourcePassphrase: passphrase.trim(),
+ sourcePassphrase: passphrase,
})
} catch {
setStatus(t('onboarding.importVault.errors.invalidFileOrPassphrase'))
@@ -189,7 +187,13 @@ const ImportVault = () => {
navigate('/home')
}
} catch (error) {
- setStatus(error instanceof Error ? error.message : t('onboarding.importVault.errors.generic'))
+ setStatus(
+ error instanceof VaultInvalidPassphraseError
+ ? t('onboarding.importVault.errors.invalidSourcePassphrase')
+ : error instanceof Error
+ ? error.message
+ : t('onboarding.importVault.errors.generic'),
+ )
setIsSaving(false)
}
}
@@ -292,7 +296,7 @@ const ImportVault = () => {
{step === 1 ? t('common.back') : t('common.previous')}
{step < TOTAL_STEPS ? (
-