Skip to content
7 changes: 3 additions & 4 deletions src/components/dapp/dapp-approval-drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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 }) => {
Expand Down
36 changes: 19 additions & 17 deletions src/components/pages/manage-accounts/account-list-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' : ''}`}
>
<div className="flex min-w-0 items-center gap-2">
<div className="min-w-0">
<div className="flex min-w-0 items-center gap-2">
<span className="min-w-0 flex-1 truncate text-sm font-semibold text-foreground">
<div className="min-w-0 flex-1">
<div className="flex min-w-0 items-start justify-between gap-2">
<div className="min-w-0">
<span className="block truncate text-sm font-semibold text-foreground">
{account.name}
</span>
</div>
<div className="flex shrink-0 items-center gap-2">
{account.watchOnly && (
<span className="inline-flex shrink-0 items-center gap-1 rounded-full border border-amber-500/30 bg-amber-500/10 px-1.5 py-0.5 text-[10px] text-amber-700 dark:text-amber-200">
<EyeIcon className="h-2.5 w-2.5" />
Expand All @@ -93,18 +95,18 @@ const AccountListItem = ({
</span>
)}
</div>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<span className="truncate">
{truncateString(account.identity, { leading: 5, trailing: 5 })}
</span>
<span className="text-[11px] font-semibold text-foreground">
{isVisible
? balance !== undefined
? formatBalanceCompact(balance)
: '--'
: HIDDEN_BALANCE}
</span>
</div>
</div>
<div className="flex items-center justify-between gap-2 text-xs text-muted-foreground">
<span className="truncate">
{truncateString(account.identity, { leading: 5, trailing: 5 })}
</span>
<span className="text-[11px] font-semibold text-foreground">
{isVisible
? balance !== undefined
? formatBalanceCompact(balance)
: '--'
: HIDDEN_BALANCE}
</span>
</div>
</div>
<div className="flex items-center gap-1">
Expand Down
6 changes: 3 additions & 3 deletions src/components/ui/input-group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',

Expand Down
4 changes: 2 additions & 2 deletions src/lib/dapp/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 4 additions & 0 deletions src/lib/lock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>) => void) => void
Expand All @@ -28,6 +29,7 @@ const getChromeSessionStorage = (): ChromeStorageSession | null => {
}

const setSessionUnlockedFlag = (value: boolean) => {
sessionUnlockedMemory = value
const sessionStorage = getChromeSessionStorage()
if (!sessionStorage) return

Expand Down Expand Up @@ -155,6 +157,8 @@ const normalizeLockTimeoutOption = (minutes: number) => {
}

const readSessionUnlockedFlag = async () => {
if (sessionUnlockedMemory) return true

const sessionStorage = getChromeSessionStorage()
if (!sessionStorage) return true

Expand Down
18 changes: 18 additions & 0 deletions src/lib/passphrase.ts
Original file line number Diff line number Diff line change
@@ -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 }
}
6 changes: 2 additions & 4 deletions src/lib/vault-password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ export const changeVaultPassphrase = async (
currentPassphrase: string,
newPassphrase: string,
): Promise<void> => {
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()
}
2 changes: 1 addition & 1 deletion src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
2 changes: 1 addition & 1 deletion src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
2 changes: 1 addition & 1 deletion src/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
2 changes: 1 addition & 1 deletion src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
2 changes: 1 addition & 1 deletion src/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,7 @@
"fileTooLarge": "Выбранный файл слишком большой для допустимого файла vault.",
"selectFile": "Выберите файл vault.",
"passphraseRequired": "Необходимо ввести пароль.",
"importFailed": "Не удалось импортировать vault веб-кошелька.",
"invalidSourcePassphrase": "Неверный пароль исходного vault.",
"noEntries": "Vault импортирован, но записи не найдены.",
"generic": "Не удалось импортировать vault.",
"invalidFileOrPassphrase": "Выбранный файл не является файлом Qubic vault или пароль введён неверно."
Expand Down
2 changes: 1 addition & 1 deletion src/locales/tr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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ış."
Expand Down
2 changes: 1 addition & 1 deletion src/locales/vi.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
2 changes: 1 addition & 1 deletion src/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,7 @@
"fileTooLarge": "所选文件过大,不是有效的 vault 文件。",
"selectFile": "请选择 vault 文件。",
"passphraseRequired": "请输入密码。",
"importFailed": "导入网页钱包 vault 失败。",
"invalidSourcePassphrase": "来源 vault 密码无效。",
"noEntries": "vault 已导入但未找到任何条目。",
"generic": "导入 vault 失败。",
"invalidFileOrPassphrase": "选择的文件不是 Qubic vault 文件,或者密码不正确。"
Expand Down
11 changes: 5 additions & 6 deletions src/pages/manage-accounts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 (
Expand Down
42 changes: 27 additions & 15 deletions src/pages/onboarding/create-wallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()) {
Expand All @@ -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'
Expand Down Expand Up @@ -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
}
Expand All @@ -184,11 +201,6 @@ const CreateWallet = ({
setStep(2)
return
}
if (passphrase.length < 12) {
setStatus(t('onboarding.errors.passphraseTooShort'))
setStep(2)
return
}
}

const cachedAccounts = getCachedAccounts()
Expand Down
Loading
Loading