diff --git a/src/components/dapp/dapp-approval-drawer.tsx b/src/components/dapp/dapp-approval-drawer.tsx index 7084eb2..dc29627 100644 --- a/src/components/dapp/dapp-approval-drawer.tsx +++ b/src/components/dapp/dapp-approval-drawer.tsx @@ -168,13 +168,12 @@ const DappApprovalDrawer = () => { return } - const normalizedPassphrase = passphrase.trim() if (approved && requiresPassphrase) { - if (!normalizedPassphrase) { + if (!passphrase.trim()) { setError(t('passphraseAuth.validation.required')) return } - const validation = await validateVaultPassphrase(normalizedPassphrase) + const validation = await validateVaultPassphrase(passphrase) if (!validation.valid) { setError( validation.reason === 'invalid' @@ -199,7 +198,7 @@ const DappApprovalDrawer = () => { payload: { id: current.id, approved, - passphrase: approved && requiresPassphrase ? normalizedPassphrase : undefined, + passphrase: approved && requiresPassphrase ? passphrase : undefined, }, }, (response?: { ok?: boolean; executed?: boolean; targetTick?: number }) => { diff --git a/src/components/pages/manage-accounts/account-list-item.tsx b/src/components/pages/manage-accounts/account-list-item.tsx index 3de2847..a6ce6e8 100644 --- a/src/components/pages/manage-accounts/account-list-item.tsx +++ b/src/components/pages/manage-accounts/account-list-item.tsx @@ -71,16 +71,18 @@ const AccountListItem = ({ } }} aria-label={`${t('accounts.manage.menu')} ${account.name}`} - className={`flex w-full items-center justify-between gap-2 rounded-lg border border-border/60 bg-card/80 px-3 py-2 ${ + className={`flex w-full items-center justify-between gap-2 rounded-lg border border-border/60 bg-card/80 pl-2 pr-3 py-2 ${ isOver ? 'ring-2 ring-primary/40' : '' } ${isDragging ? 'opacity-60' : ''}`} > -
-
-
- +
+
+
+ {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 ? ( - diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index 35ec28a..46f8c58 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -63,8 +63,8 @@ const Settings = () => { setExportError('') try { - const vault = await openBrowserVault(exportPassphrase.trim(), false) - const encryptedVault = await exportVaultToWebWalletFormat(vault, exportPassphrase.trim()) + const vault = await openBrowserVault(exportPassphrase, false) + const encryptedVault = await exportVaultToWebWalletFormat(vault, exportPassphrase) const json = JSON.stringify(encryptedVault, null, 2) const blob = new Blob([new TextEncoder().encode(json)], { diff --git a/src/router/app-router.tsx b/src/router/app-router.tsx index 712dad2..c476ce0 100644 --- a/src/router/app-router.tsx +++ b/src/router/app-router.tsx @@ -48,6 +48,7 @@ const AppRouter = () => { const location = useLocation() const isOnboarded = hasAccounts() const [isLocked, setIsLocked] = useState(() => isWalletLocked()) + const currentlyLocked = isOnboarded ? isWalletLocked() : isLocked const lockTimeoutRef = useRef(null) const hideChrome = location.pathname === '/unlock' const routeKey = `${location.pathname}${location.search}` @@ -214,7 +215,7 @@ const AppRouter = () => { ) } - if (isLocked && location.pathname !== '/unlock') { + if (currentlyLocked && location.pathname !== '/unlock') { return }