From 9d3004f15fb065d55b0a260fbc9dc2af8344b8db Mon Sep 17 00:00:00 2001 From: highlander Date: Mon, 2 Mar 2026 16:52:30 -0700 Subject: [PATCH 01/15] =?UTF-8?q?fix:=20bootloader/firmware=20wizard=20UX?= =?UTF-8?q?=20=E2=80=94=20correct=20version=20detection=20and=20conditiona?= =?UTF-8?q?l=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix false needsBootloaderUpdate when device already in bootloader mode (extractVersion returns BL version as fwVersion; use it for comparison) - Fix needsFirmwareUpdate in bootloader mode (always true, was comparing BL version against latest firmware) - Show hold-and-connect SVG + instructions only when device is NOT in bootloader mode; auto-start update when already in bootloader - Add pulsing "confirm on device" box and striped progress bar during firmware upload - Add verify-backup-skip hints for both bootloader and firmware updates - Add i18n keys for new UX elements Co-Authored-By: Claude Opus 4.6 --- .../src/bun/engine-controller.ts | 17 +- .../mainview/components/OobSetupWizard.tsx | 309 ++++++++++++------ .../src/mainview/i18n/locales/en/setup.json | 14 +- 3 files changed, 240 insertions(+), 100 deletions(-) diff --git a/projects/keepkey-vault/src/bun/engine-controller.ts b/projects/keepkey-vault/src/bun/engine-controller.ts index 3e6c96b..10f0e90 100644 --- a/projects/keepkey-vault/src/bun/engine-controller.ts +++ b/projects/keepkey-vault/src/bun/engine-controller.ts @@ -488,16 +488,25 @@ export class EngineController extends EventEmitter { const blVersion = features?.bootloaderVersion || undefined const bootloaderMode = features?.bootloaderMode ?? false const initialized = features?.initialized ?? false - const needsFw = fwVersion - ? (this.versionLessThan(fwVersion, this.latestFirmware) || fwVersion === '4.0.0') - : false + // In bootloader mode, fwVersion is actually the BL version (from extractVersion). + // Firmware always needs flashing when device is in bootloader mode. + const needsFw = bootloaderMode + ? true + : fwVersion + ? (this.versionLessThan(fwVersion, this.latestFirmware) || fwVersion === '4.0.0') + : false // Bootloader version check with hash-to-version fallback. // Some firmware versions don't report blVersion in features, but DO report // blHash. Use the manifest to resolve hash → version and avoid a false // "needs bootloader update" that causes an infinite update loop. let effectiveBlVersion = blVersion - if (!effectiveBlVersion && !bootloaderMode && features) { + if (!effectiveBlVersion && bootloaderMode && fwVersion) { + // In bootloader mode, majorVersion/minorVersion/patchVersion IS the BL version. + // extractVersion() returns it as fwVersion — use it for comparison. + effectiveBlVersion = fwVersion + console.log(`[Engine] Bootloader mode: using extractVersion ${fwVersion} as BL version`) + } else if (!effectiveBlVersion && !bootloaderMode && features) { const blHash = base64ToHex(features.bootloaderHash) if (blHash && this.manifest?.hashes?.bootloader) { const resolved = this.manifest.hashes.bootloader[blHash] diff --git a/projects/keepkey-vault/src/mainview/components/OobSetupWizard.tsx b/projects/keepkey-vault/src/mainview/components/OobSetupWizard.tsx index 1b14015..9425b96 100644 --- a/projects/keepkey-vault/src/mainview/components/OobSetupWizard.tsx +++ b/projects/keepkey-vault/src/mainview/components/OobSetupWizard.tsx @@ -9,6 +9,7 @@ import { FaExclamationTriangle, FaPlus, } from 'react-icons/fa' +import holdAndConnectSvg from '../assets/svg/hold-and-connect.svg' import { useFirmwareUpdate } from '../hooks/useFirmwareUpdate' import { useDeviceState } from '../hooks/useDeviceState' import { rpcRequest } from '../lib/rpc' @@ -27,6 +28,15 @@ const ANIMATIONS_CSS = ` 50% { transform: scale(1.1); } 100% { transform: scale(1); } } + @keyframes kkStripe { + 0% { background-position: 0 0; } + 100% { background-position: 40px 0; } + } + @keyframes kkGlow { + 0% { box-shadow: 0 0 8px rgba(251, 146, 60, 0.4); } + 50% { box-shadow: 0 0 20px rgba(251, 146, 60, 0.7); } + 100% { box-shadow: 0 0 8px rgba(251, 146, 60, 0.4); } + } ` // ── Step definitions ──────────────────────────────────────────────────────── @@ -208,6 +218,14 @@ export function OobSetupWizard({ onComplete }: OobSetupWizardProps) { } }, [waitingForBootloader, deviceStatus.bootloaderMode, updateState, startBootloaderUpdate]) + // Auto-start: if device is already in bootloader mode when we reach this step + useEffect(() => { + if (step !== 'bootloader') return + if (updateState !== 'idle') return + if (!inBootloader) return + startBootloaderUpdate() + }, [step, updateState, inBootloader, startBootloaderUpdate]) + useEffect(() => { if (step !== 'bootloader') return if (updateState === 'complete') { @@ -553,48 +571,148 @@ export function OobSetupWizard({ onComplete }: OobSetupWizardProps) { {/* ═══════════════ BOOTLOADER ════════════════════════════ */} {step === 'bootloader' && ( - - - - {t('bootloader.title')} - - - {t('bootloader.description')} - - + {/* ── Not yet in bootloader: show instructions to enter it ── */} + {!inBootloader && updateState !== 'updating' && updateState !== 'error' && ( + <> + {/* Animated hold-and-connect illustration */} + + Hold button and connect USB + + + + {t('bootloader.title')} + + + {t('bootloader.description')} + + - {/* Version info */} - {deviceStatus.latestBootloader && ( - - - - {t('bootloader.current')} - - {(deviceStatus.bootloaderVersion && !deviceStatus.bootloaderVersion.startsWith('hash:')) - ? `v${deviceStatus.bootloaderVersion}` - : inBootloader - ? `v${deviceStatus.firmwareVersion}` - : t('bootloader.outdated')} - + {/* Version info */} + {deviceStatus.latestBootloader && ( + + + + {t('bootloader.current')} + + {(deviceStatus.bootloaderVersion && !deviceStatus.bootloaderVersion.startsWith('hash:')) + ? `v${deviceStatus.bootloaderVersion}` + : t('bootloader.outdated')} + + + + + {t('bootloader.latest')} + + v{deviceStatus.latestBootloader} + + + + + )} + + {/* Instructions to enter bootloader mode */} + + + + + + {t('bootloader.enterFirmwareUpdateMode')} + + + + {t('bootloader.step1Unplug')} + {t('bootloader.step2Hold')} + {t('bootloader.step3Plugin')} + {t('bootloader.step4Release')} + - - - {t('bootloader.latest')} - - v{deviceStatus.latestBootloader} + + + {/* Waiting indicator */} + {waitingForBootloader && ( + + + + {t('bootloader.listeningForBootloader')} - - - + + )} + + {/* Action buttons — idle only */} + {!waitingForBootloader && ( + <> + + + + + )} + )} - {/* Updating */} + {/* ── Already in bootloader / updating ── */} {updateState === 'updating' && ( + + + {t('bootloader.title')} + {updateProgress?.message || t('bootloader.updatingBootloader')} + + {/* Version info during update */} + {deviceStatus.latestBootloader && ( + + + + {t('bootloader.current')} + + v{deviceStatus.firmwareVersion || '?'} + + + + + {t('bootloader.latest')} + + v{deviceStatus.latestBootloader} + + + + + )} + + {/* Verify backup skip hint */} + + + + + + + {t('bootloader.verifyBackupHint')} + + + @@ -625,65 +743,6 @@ export function OobSetupWizard({ onComplete }: OobSetupWizardProps) { )} - - {/* Instructions */} - {updateState !== 'updating' && updateState !== 'error' && ( - - - - - - {t('bootloader.enterFirmwareUpdateMode')} - - - - {t('bootloader.step1Unplug')} - {t('bootloader.step2Hold')} - {t('bootloader.step3Plugin')} - {t('bootloader.step4Release')} - - - - )} - - {/* Waiting indicator */} - {waitingForBootloader && updateState !== 'updating' && ( - - - - {t('bootloader.listeningForBootloader')} - - - )} - - {/* Action buttons — idle only */} - {updateState !== 'updating' && updateState !== 'error' && !waitingForBootloader && ( - <> - - - - - )} )} @@ -742,10 +801,72 @@ export function OobSetupWizard({ onComplete }: OobSetupWizardProps) { {/* Update in progress */} {updateState === 'updating' && ( - - - {updateProgress?.message || t('firmware.updatingFirmware')} - + {/* Confirm on device — pulsing orange box */} + + + + {t('firmware.confirmOnDevice')} + + + {t('firmware.lookAtDeviceAndPress')} + + + + + {/* Verify backup note */} + + + + + + + {t('firmware.verifyBackupNote')} + + + + + {/* Progress bar */} + + + {t('firmware.uploadingFirmware')} + + + + + {updateProgress?.percent != null && ( + + {Math.round(updateProgress.percent)}% + + )} + + {t('firmware.deviceWillRestart')} + + + diff --git a/projects/keepkey-vault/src/mainview/i18n/locales/en/setup.json b/projects/keepkey-vault/src/mainview/i18n/locales/en/setup.json index 6884b9a..2f01219 100644 --- a/projects/keepkey-vault/src/mainview/i18n/locales/en/setup.json +++ b/projects/keepkey-vault/src/mainview/i18n/locales/en/setup.json @@ -39,7 +39,10 @@ "step4Release": "4. Release when the bootloader screen appears", "listeningForBootloader": "Listening for device in bootloader mode...", "readyDetectBootloader": "I'm Ready — Detect Bootloader", - "skipBootloaderUpdate": "Skip Bootloader Update" + "skipBootloaderUpdate": "Skip Bootloader Update", + "verifyBackupHint": "On the KeepKey, it will ask you to verify backup. We will do this after updating — hold the button to skip this for now.", + "followDirectionsOnDevice": "Follow directions on device", + "deviceWillGuide": "Your KeepKey will guide you through the update process." }, "firmware": { "title": "Firmware Update", @@ -58,7 +61,14 @@ "firmwareVerified": "Firmware verified as official release", "firmwareHashNotFound": "Firmware hash not found in manifest", "updateFirmwareTo": "Update Firmware to v{{version}}", - "skipUpdate": "Skip Update" + "skipUpdate": "Skip Update", + "confirmOnDevice": "Confirm action on device!", + "lookAtDeviceAndPress": "Look at your KeepKey screen and press the button to confirm.", + "verifyBackupNote": "If your device is not set up, you can safely ignore any \"verify backup\" screen.", + "uploadingFirmware": "Uploading firmware... Do not disconnect your device", + "estimatedTimeRemaining": "Estimated time remaining: {{seconds}}s", + "deviceWillRestart": "Your device will restart when complete.", + "skipWarning": "You can continue with older firmware. Some features may not work as expected." }, "initChoose": { "title": "Set Up Your Wallet", From 16107512fa8966c5ab5e9b89fa52039d473f5e5f Mon Sep 17 00:00:00 2001 From: highlander Date: Mon, 2 Mar 2026 16:54:52 -0700 Subject: [PATCH 02/15] =?UTF-8?q?fix:=20declutter=20firmware=20update=20UI?= =?UTF-8?q?=20=E2=80=94=20consolidate=20redundant=20messaging?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Hide "Important" box once update starts (shown only in idle state) - Merge "confirm on device" and "verify backup" into single pulsing box - Remove separate red "do not unplug" box, inline it under progress bar - Remove redundant "uploading firmware" and "device will restart" labels - Progress bar now shows percentage left, do-not-unplug right Co-Authored-By: Claude Opus 4.6 --- .../mainview/components/OobSetupWizard.tsx | 62 +++++++------------ 1 file changed, 21 insertions(+), 41 deletions(-) diff --git a/projects/keepkey-vault/src/mainview/components/OobSetupWizard.tsx b/projects/keepkey-vault/src/mainview/components/OobSetupWizard.tsx index 9425b96..3640200 100644 --- a/projects/keepkey-vault/src/mainview/components/OobSetupWizard.tsx +++ b/projects/keepkey-vault/src/mainview/components/OobSetupWizard.tsx @@ -786,22 +786,24 @@ export function OobSetupWizard({ onComplete }: OobSetupWizardProps) { - {/* Important instructions */} - - - - {t('firmware.important')} - - {t('firmware.doNotDisconnect')} - {t('firmware.mayNeedConfirm')} - {t('firmware.fundsRemainSafe')} - - + {/* Important instructions — only before update starts */} + {updateState === 'idle' && ( + + + + {t('firmware.important')} + + {t('firmware.doNotDisconnect')} + {t('firmware.mayNeedConfirm')} + {t('firmware.fundsRemainSafe')} + + + )} {/* Update in progress */} {updateState === 'updating' && ( - {/* Confirm on device — pulsing orange box */} + {/* Confirm on device + verify backup — single box */} {t('firmware.lookAtDeviceAndPress')} - - - - {/* Verify backup note */} - - - - - - + {t('firmware.verifyBackupNote')} - + {/* Progress bar */} - - {t('firmware.uploadingFirmware')} - - {updateProgress?.percent != null && ( - - {Math.round(updateProgress.percent)}% - - )} - - {t('firmware.deviceWillRestart')} - - - - - - - {t('firmware.doNotUnplug')} + + {updateProgress?.percent != null ? ( + {Math.round(updateProgress.percent)}% + ) : } + {t('firmware.doNotUnplug')} From 50c557438ab0a4a6fda702324483e96f1871d29b Mon Sep 17 00:00:00 2001 From: highlander Date: Mon, 2 Mar 2026 16:57:25 -0700 Subject: [PATCH 03/15] =?UTF-8?q?fix:=20firmware=20update=20requires=20boo?= =?UTF-8?q?tloader=20mode=20=E2=80=94=20add=20entry=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Firmware flashing (firmwareErase + firmwareUpload) requires the device to be in bootloader mode. Previously the wizard blindly called startFirmwareUpdate() which failed with "Not in bootloader mode". Now the firmware step mirrors the bootloader step's flow: - If device is NOT in bootloader mode: show hold-and-connect SVG + 4-step instructions + "I'm Ready" button that polls for bootloader - If device IS in bootloader mode: auto-start firmware update - Event-driven detection via deviceStatus.bootloaderMode push Co-Authored-By: Claude Opus 4.6 --- .../mainview/components/OobSetupWizard.tsx | 86 ++++++++++++++++++- 1 file changed, 82 insertions(+), 4 deletions(-) diff --git a/projects/keepkey-vault/src/mainview/components/OobSetupWizard.tsx b/projects/keepkey-vault/src/mainview/components/OobSetupWizard.tsx index 3640200..c27f6eb 100644 --- a/projects/keepkey-vault/src/mainview/components/OobSetupWizard.tsx +++ b/projects/keepkey-vault/src/mainview/components/OobSetupWizard.tsx @@ -123,6 +123,7 @@ export function OobSetupWizard({ onComplete }: OobSetupWizardProps) { // Bootloader state const [waitingForBootloader, setWaitingForBootloader] = useState(false) + const [waitingForBootloaderFw, setWaitingForBootloaderFw] = useState(false) const bootloaderPollRef = useRef | null>(null) // Hooks — use Electrobun RPC-based hooks @@ -262,7 +263,48 @@ export function OobSetupWizard({ onComplete }: OobSetupWizardProps) { // ── Firmware step ────────────────────────────────────────────────────── + const handleEnterBootloaderForFirmware = () => { + setWaitingForBootloaderFw(true) + bootloaderPollRef.current = setInterval(async () => { + try { + const state = await rpcRequest('getDeviceState') + if (state.bootloaderMode) { + setWaitingForBootloaderFw(false) + if (bootloaderPollRef.current) clearInterval(bootloaderPollRef.current) + await startFirmwareUpdate(deviceStatus.latestFirmware || undefined) + } + } catch { + // Device may be disconnecting/reconnecting + } + }, 2000) + } + + // Event-driven: detect bootloader mode for firmware step + useEffect(() => { + if (step !== 'firmware') return + if (!waitingForBootloaderFw) return + if (updateState === 'updating') return + + if (deviceStatus.bootloaderMode) { + setWaitingForBootloaderFw(false) + if (bootloaderPollRef.current) clearInterval(bootloaderPollRef.current) + startFirmwareUpdate(deviceStatus.latestFirmware || undefined) + } + }, [step, waitingForBootloaderFw, deviceStatus.bootloaderMode, updateState, startFirmwareUpdate, deviceStatus.latestFirmware]) + + // Auto-start firmware update if already in bootloader mode + useEffect(() => { + if (step !== 'firmware') return + if (updateState !== 'idle') return + if (!inBootloader) return + startFirmwareUpdate(deviceStatus.latestFirmware || undefined) + }, [step, updateState, inBootloader, startFirmwareUpdate, deviceStatus.latestFirmware]) + const handleStartFirmwareUpdate = async () => { + if (!inBootloader) { + handleEnterBootloaderForFirmware() + return + } await startFirmwareUpdate(deviceStatus.latestFirmware || undefined) } @@ -786,8 +828,42 @@ export function OobSetupWizard({ onComplete }: OobSetupWizardProps) { - {/* Important instructions — only before update starts */} - {updateState === 'idle' && ( + {/* Needs bootloader mode — show entry instructions */} + {updateState === 'idle' && !inBootloader && ( + <> + + Hold button and connect USB + + + + + + + {t('bootloader.enterFirmwareUpdateMode')} + + + + {t('bootloader.step1Unplug')} + {t('bootloader.step2Hold')} + {t('bootloader.step3Plugin')} + {t('bootloader.step4Release')} + + + + + {waitingForBootloaderFw && ( + + + + {t('bootloader.listeningForBootloader')} + + + )} + + )} + + {/* Ready to flash — device in bootloader mode */} + {updateState === 'idle' && inBootloader && ( @@ -906,7 +982,7 @@ export function OobSetupWizard({ onComplete }: OobSetupWizardProps) { )} {/* Actions — idle */} - {updateState === 'idle' && ( + {updateState === 'idle' && !waitingForBootloaderFw && ( {!isOobDevice && ( - {!isOobDevice && ( - - )} - + {/* Skip — only for non-OOB devices, only when idle */} + {updateState === 'idle' && !isOobDevice && ( + )} )} From 668f2461fa9e0db4e153f567148749beb3e240f3 Mon Sep 17 00:00:00 2001 From: highlander Date: Mon, 2 Mar 2026 17:13:15 -0700 Subject: [PATCH 05/15] fix: remove RPC timeouts for device-interactive ops, refine setup UX - RPC: timeoutMs=0 means no timeout (was 600s), used for all device-interactive ops (firmware flash, seed verify, PIN, wipe) - Electrobun RPC maxRequestTime set to Infinity - Setup wizard: collapsible seed length selector on create card, prominent selector on recovery card, remove top-level word count bar - Remove cursor:pointer from non-clickable create card container Co-Authored-By: Claude Opus 4.6 --- projects/keepkey-vault/src/bun/index.ts | 2 +- .../components/DeviceSettingsDrawer.tsx | 6 +- .../mainview/components/FirmwareDropZone.tsx | 4 +- .../mainview/components/OobSetupWizard.tsx | 149 +++++++++++++----- .../src/mainview/hooks/useFirmwareUpdate.ts | 4 +- .../src/mainview/hooks/useUpdateState.ts | 2 +- .../keepkey-vault/src/mainview/lib/rpc.ts | 17 +- 7 files changed, 132 insertions(+), 52 deletions(-) diff --git a/projects/keepkey-vault/src/bun/index.ts b/projects/keepkey-vault/src/bun/index.ts index c40fd07..e8b9f9d 100644 --- a/projects/keepkey-vault/src/bun/index.ts +++ b/projects/keepkey-vault/src/bun/index.ts @@ -245,7 +245,7 @@ if (!restApiEnabled) console.log('[Vault] REST API disabled (enable in Settings // ── RPC Bridge (Electrobun UI ↔ Bun) ───────────────────────────────── const rpc = BrowserView.defineRPC({ - maxRequestTime: 600000, // device-interactive ops (recovery, create) can take 5-10 minutes + maxRequestTime: Infinity, // no timeout — user can take as long as needed to confirm on device handlers: { requests: { // ── Device lifecycle ────────────────────────────────────── diff --git a/projects/keepkey-vault/src/mainview/components/DeviceSettingsDrawer.tsx b/projects/keepkey-vault/src/mainview/components/DeviceSettingsDrawer.tsx index c2c2c9b..8caa925 100644 --- a/projects/keepkey-vault/src/mainview/components/DeviceSettingsDrawer.tsx +++ b/projects/keepkey-vault/src/mainview/components/DeviceSettingsDrawer.tsx @@ -209,7 +209,7 @@ export function DeviceSettingsDrawer({ open, onClose, deviceState, onCheckForUpd setVerifying(true) setVerifyResult(null) try { - const result = await rpcRequest("verifySeed", { wordCount: verifyWordCount }, 600000) as { success: boolean; message: string } + const result = await rpcRequest("verifySeed", { wordCount: verifyWordCount }, 0) as { success: boolean; message: string } setVerifyResult(result) } catch (e: any) { const msg = typeof e?.message === "string" ? e.message : t("verificationFailed") @@ -221,7 +221,7 @@ export function DeviceSettingsDrawer({ open, onClose, deviceState, onCheckForUpd const wipeDevice = useCallback(async () => { setWiping(true) try { - await rpcRequest("wipeDevice", undefined, 60000) + await rpcRequest("wipeDevice", undefined, 0) } catch (e: any) { console.error("wipeDevice:", e) } setWiping(false) setWipeConfirm(false) @@ -274,7 +274,7 @@ export function DeviceSettingsDrawer({ open, onClose, deviceState, onCheckForUpd const handleChangePin = useCallback(async () => { setChangingPin(true) try { - await rpcRequest("changePin", undefined, 600000) + await rpcRequest("changePin", undefined, 0) } catch (e: any) { console.error("changePin:", e) } setChangingPin(false) // Refresh features diff --git a/projects/keepkey-vault/src/mainview/components/FirmwareDropZone.tsx b/projects/keepkey-vault/src/mainview/components/FirmwareDropZone.tsx index 273a6ab..e379412 100644 --- a/projects/keepkey-vault/src/mainview/components/FirmwareDropZone.tsx +++ b/projects/keepkey-vault/src/mainview/components/FirmwareDropZone.tsx @@ -123,8 +123,8 @@ export function FirmwareDropZone() { setPhase("flashing") setProgress({ percent: 0, message: "Starting firmware flash..." }) try { - // 10 min timeout — firmware erase + upload can be slow - await rpcRequest("flashCustomFirmware", { data: fileDataB64 }, 600000) + // No timeout — user must confirm on device, can take as long as needed + await rpcRequest("flashCustomFirmware", { data: fileDataB64 }, 0) // Progress events drive phase to "complete" via the firmware-progress listener } catch (err: any) { setError(err?.message || "Firmware flash failed") diff --git a/projects/keepkey-vault/src/mainview/components/OobSetupWizard.tsx b/projects/keepkey-vault/src/mainview/components/OobSetupWizard.tsx index ff26777..990ac2a 100644 --- a/projects/keepkey-vault/src/mainview/components/OobSetupWizard.tsx +++ b/projects/keepkey-vault/src/mainview/components/OobSetupWizard.tsx @@ -8,6 +8,8 @@ import { FaCheckCircle, FaExclamationTriangle, FaPlus, + FaChevronDown, + FaChevronUp, } from 'react-icons/fa' import holdAndConnectSvg from '../assets/svg/hold-and-connect.svg' import { useFirmwareUpdate } from '../hooks/useFirmwareUpdate' @@ -117,6 +119,9 @@ export function OobSetupWizard({ onComplete, onSetupInProgress }: OobSetupWizard { id: 'init-choose', label: t('visibleSteps.setup'), number: 3 }, ] + // Advanced seed length toggle for create wallet + const [showCreateAdvanced, setShowCreateAdvanced] = useState(false) + // Dev: load-device dialog const [devLoadOpen, setDevLoadOpen] = useState(false) const [devSeed, setDevSeed] = useState('') @@ -349,8 +354,8 @@ export function OobSetupWizard({ onComplete, onSetupInProgress }: OobSetupWizard // ── Init: Create / Recover ───────────────────────────────────────────── - // Device-interactive ops need 10 min timeout — user enters seed words on device - const DEVICE_INTERACTION_TIMEOUT = 600000 + // No timeout for device-interactive ops — user can take as long as needed + const DEVICE_INTERACTION_TIMEOUT = 0 const handleCreateWallet = async () => { setSetupType('create') @@ -995,36 +1000,6 @@ export function OobSetupWizard({ onComplete, onSetupInProgress }: OobSetupWizard - {/* Word count selector */} - - - {t('initChoose.seedLength', { defaultValue: 'Recovery seed length' })} - - - {([12, 18, 24] as const).map((wc) => ( - setWordCount(wc)} - > - {wc} {t('initChoose.words', { defaultValue: 'words' })} - - ))} - - - {setupError && ( {setupError} @@ -1048,13 +1023,11 @@ export function OobSetupWizard({ onComplete, onSetupInProgress }: OobSetupWizard borderWidth="2px" borderColor="transparent" bg="gray.700" - cursor="pointer" transition="all 0.2s" _hover={{ borderColor: 'orange.500', transform: 'translateY(-2px)', }} - onClick={handleCreateWallet} > @@ -1068,6 +1041,78 @@ export function OobSetupWizard({ onComplete, onSetupInProgress }: OobSetupWizard {t('initChoose.createDescription')} + + {/* Collapsible seed length for create */} + + { + e.stopPropagation() + setShowCreateAdvanced(!showCreateAdvanced) + }} + _hover={{ opacity: 0.8 }} + > + + {t('initChoose.seedLength', { defaultValue: 'Seed length' })}: {wordCount} {t('initChoose.words', { defaultValue: 'words' })} + + {showCreateAdvanced + ? + : + } + + {showCreateAdvanced && ( + + + {([12, 18, 24] as const).map((wc) => ( + { + e.stopPropagation() + setWordCount(wc) + }} + > + {wc} + + ))} + + + {t('initChoose.entropyNote', { defaultValue: 'Added seed length does not improve overall wallet entropy.' })}{' '} + e.stopPropagation()} + > + {t('initChoose.learnMore', { defaultValue: 'Learn more' })} + + + + )} + + - )} - {/* Skip — only for non-OOB devices, only when idle */} {updateState === 'idle' && !isOobDevice && ( + {onWipe && ( + setShowWipeConfirm(true)} + > + {t("pin.forgotPinWipe")} + + )} + + )} + + {/* Wipe confirmation panel */} + {showWipeConfirm && ( + - {t("cancel", { ns: "common" })} - + + {t("pin.wipeWarningTitle")} + + + {t("pin.wipeWarningDescription")} + + + setWipeAcknowledged(e.target.checked)} + mt="1" + accentColor="#FFD700" + /> + + {t("pin.wipeAcknowledge")} + + + + + + + )} diff --git a/projects/keepkey-vault/src/mainview/i18n/locales/de/device.json b/projects/keepkey-vault/src/mainview/i18n/locales/de/device.json index 41c688f..d64cb27 100644 --- a/projects/keepkey-vault/src/mainview/i18n/locales/de/device.json +++ b/projects/keepkey-vault/src/mainview/i18n/locales/de/device.json @@ -7,7 +7,13 @@ "createDescription": "Schauen Sie auf den Bildschirm Ihres KeepKey und tippen Sie auf die Positionen, um eine neue PIN festzulegen", "confirmDescription": "Geben Sie die gleiche PIN erneut ein, um zu bestätigen", "backspace": "Rücktaste", - "unlock": "Entsperren" + "unlock": "Entsperren", + "forgotPinWipe": "PIN vergessen? Gerät löschen", + "wipeWarningTitle": "KeepKey löschen", + "wipeWarningDescription": "Das Löschen Ihres KeepKey entfernt alle Schlüssel und Einstellungen. Sie verlieren den Zugang zu Ihren Mitteln, wenn Sie Ihre Wiederherstellungsphrase nicht haben.", + "wipeAcknowledge": "Ich habe meine Wiederherstellungsphrase gesichert und verstehe, was ich tue", + "wipeDevice": "Gerät löschen", + "wiping": "Lösche..." }, "passphrase": { "title": "Passphrase eingeben", diff --git a/projects/keepkey-vault/src/mainview/i18n/locales/en/device.json b/projects/keepkey-vault/src/mainview/i18n/locales/en/device.json index 57c569a..b343540 100644 --- a/projects/keepkey-vault/src/mainview/i18n/locales/en/device.json +++ b/projects/keepkey-vault/src/mainview/i18n/locales/en/device.json @@ -7,7 +7,13 @@ "createDescription": "Look at your KeepKey screen and tap the positions to set a new PIN", "confirmDescription": "Enter the same PIN again to confirm", "backspace": "Backspace", - "unlock": "Unlock" + "unlock": "Unlock", + "forgotPinWipe": "Forgot your PIN? Wipe device", + "wipeWarningTitle": "Wipe Your KeepKey", + "wipeWarningDescription": "Wiping your KeepKey will erase all keys and settings. You will lose access to your funds if you don't have your recovery phrase.", + "wipeAcknowledge": "I have backed up my recovery phrase and understand what I am doing", + "wipeDevice": "Wipe Device", + "wiping": "Wiping..." }, "passphrase": { "title": "Enter Passphrase", diff --git a/projects/keepkey-vault/src/mainview/i18n/locales/es/device.json b/projects/keepkey-vault/src/mainview/i18n/locales/es/device.json index a06f88d..a67b63a 100644 --- a/projects/keepkey-vault/src/mainview/i18n/locales/es/device.json +++ b/projects/keepkey-vault/src/mainview/i18n/locales/es/device.json @@ -7,7 +7,13 @@ "createDescription": "Mira la pantalla de tu KeepKey y toca las posiciones para establecer un nuevo PIN", "confirmDescription": "Ingresa el mismo PIN nuevamente para confirmar", "backspace": "Retroceso", - "unlock": "Desbloquear" + "unlock": "Desbloquear", + "forgotPinWipe": "¿Olvidaste tu PIN? Borrar dispositivo", + "wipeWarningTitle": "Borrar tu KeepKey", + "wipeWarningDescription": "Borrar tu KeepKey eliminará todas las claves y configuraciones. Perderás acceso a tus fondos si no tienes tu frase de recuperación.", + "wipeAcknowledge": "He respaldado mi frase de recuperación y entiendo lo que estoy haciendo", + "wipeDevice": "Borrar dispositivo", + "wiping": "Borrando..." }, "passphrase": { "title": "Ingresar contraseña", diff --git a/projects/keepkey-vault/src/mainview/i18n/locales/fr/device.json b/projects/keepkey-vault/src/mainview/i18n/locales/fr/device.json index 72858ba..ee15b92 100644 --- a/projects/keepkey-vault/src/mainview/i18n/locales/fr/device.json +++ b/projects/keepkey-vault/src/mainview/i18n/locales/fr/device.json @@ -7,7 +7,13 @@ "createDescription": "Regardez l'écran de votre KeepKey et appuyez sur les positions pour définir un nouveau PIN", "confirmDescription": "Entrez le même PIN à nouveau pour confirmer", "backspace": "Retour arrière", - "unlock": "Déverrouiller" + "unlock": "Déverrouiller", + "forgotPinWipe": "PIN oublié ? Effacer l'appareil", + "wipeWarningTitle": "Effacer votre KeepKey", + "wipeWarningDescription": "Effacer votre KeepKey supprimera toutes les clés et paramètres. Vous perdrez l'accès à vos fonds si vous n'avez pas votre phrase de récupération.", + "wipeAcknowledge": "J'ai sauvegardé ma phrase de récupération et je comprends ce que je fais", + "wipeDevice": "Effacer l'appareil", + "wiping": "Effacement..." }, "passphrase": { "title": "Entrer la phrase secrète", diff --git a/projects/keepkey-vault/src/mainview/i18n/locales/it/device.json b/projects/keepkey-vault/src/mainview/i18n/locales/it/device.json index b8114e5..fbfee0d 100644 --- a/projects/keepkey-vault/src/mainview/i18n/locales/it/device.json +++ b/projects/keepkey-vault/src/mainview/i18n/locales/it/device.json @@ -7,7 +7,13 @@ "createDescription": "Guarda lo schermo del tuo KeepKey e tocca le posizioni per impostare un nuovo PIN", "confirmDescription": "Inserisci lo stesso PIN di nuovo per confermare", "backspace": "Cancella", - "unlock": "Sblocca" + "unlock": "Sblocca", + "forgotPinWipe": "PIN dimenticato? Cancella dispositivo", + "wipeWarningTitle": "Cancella il tuo KeepKey", + "wipeWarningDescription": "Cancellare il tuo KeepKey eliminerà tutte le chiavi e le impostazioni. Perderai l'accesso ai tuoi fondi se non hai la tua frase di recupero.", + "wipeAcknowledge": "Ho fatto il backup della mia frase di recupero e capisco cosa sto facendo", + "wipeDevice": "Cancella dispositivo", + "wiping": "Cancellazione..." }, "passphrase": { "title": "Inserisci passphrase", diff --git a/projects/keepkey-vault/src/mainview/i18n/locales/ja/device.json b/projects/keepkey-vault/src/mainview/i18n/locales/ja/device.json index 9117073..60fc5b2 100644 --- a/projects/keepkey-vault/src/mainview/i18n/locales/ja/device.json +++ b/projects/keepkey-vault/src/mainview/i18n/locales/ja/device.json @@ -7,7 +7,13 @@ "createDescription": "KeepKeyの画面を見て、位置をタップして新しいPINを設定してください", "confirmDescription": "確認のため同じPINをもう一度入力してください", "backspace": "バックスペース", - "unlock": "ロック解除" + "unlock": "ロック解除", + "forgotPinWipe": "PINを忘れましたか?デバイスを消去", + "wipeWarningTitle": "KeepKeyを消去", + "wipeWarningDescription": "KeepKeyを消去すると、すべてのキーと設定が削除されます。リカバリーフレーズがない場合、資金へのアクセスを失います。", + "wipeAcknowledge": "リカバリーフレーズをバックアップしており、自分が何をしているか理解しています", + "wipeDevice": "デバイスを消去", + "wiping": "消去中..." }, "passphrase": { "title": "パスフレーズを入力", diff --git a/projects/keepkey-vault/src/mainview/i18n/locales/ko/device.json b/projects/keepkey-vault/src/mainview/i18n/locales/ko/device.json index 6e045c7..1662838 100644 --- a/projects/keepkey-vault/src/mainview/i18n/locales/ko/device.json +++ b/projects/keepkey-vault/src/mainview/i18n/locales/ko/device.json @@ -7,7 +7,13 @@ "createDescription": "KeepKey 화면을 보고 위치를 탭하여 새 PIN을 설정하세요", "confirmDescription": "확인을 위해 동일한 PIN을 다시 입력하세요", "backspace": "백스페이스", - "unlock": "잠금 해제" + "unlock": "잠금 해제", + "forgotPinWipe": "PIN을 잊으셨나요? 기기 초기화", + "wipeWarningTitle": "KeepKey 초기화", + "wipeWarningDescription": "KeepKey를 초기화하면 모든 키와 설정이 삭제됩니다. 복구 문구가 없으면 자금에 접근할 수 없게 됩니다.", + "wipeAcknowledge": "복구 문구를 백업했으며 제가 무엇을 하고 있는지 이해합니다", + "wipeDevice": "기기 초기화", + "wiping": "초기화 중..." }, "passphrase": { "title": "비밀번호 구문 입력", diff --git a/projects/keepkey-vault/src/mainview/i18n/locales/pt/device.json b/projects/keepkey-vault/src/mainview/i18n/locales/pt/device.json index bd406a5..bb01c1c 100644 --- a/projects/keepkey-vault/src/mainview/i18n/locales/pt/device.json +++ b/projects/keepkey-vault/src/mainview/i18n/locales/pt/device.json @@ -7,7 +7,13 @@ "createDescription": "Olhe para a tela do seu KeepKey e toque nas posições para definir um novo PIN", "confirmDescription": "Insira o mesmo PIN novamente para confirmar", "backspace": "Retrocesso", - "unlock": "Desbloquear" + "unlock": "Desbloquear", + "forgotPinWipe": "Esqueceu seu PIN? Limpar dispositivo", + "wipeWarningTitle": "Limpar seu KeepKey", + "wipeWarningDescription": "Limpar seu KeepKey apagará todas as chaves e configurações. Você perderá acesso aos seus fundos se não tiver sua frase de recuperação.", + "wipeAcknowledge": "Fiz backup da minha frase de recuperação e entendo o que estou fazendo", + "wipeDevice": "Limpar dispositivo", + "wiping": "Limpando..." }, "passphrase": { "title": "Inserir frase-senha", diff --git a/projects/keepkey-vault/src/mainview/i18n/locales/ru/device.json b/projects/keepkey-vault/src/mainview/i18n/locales/ru/device.json index 67e5d01..e320546 100644 --- a/projects/keepkey-vault/src/mainview/i18n/locales/ru/device.json +++ b/projects/keepkey-vault/src/mainview/i18n/locales/ru/device.json @@ -7,7 +7,13 @@ "createDescription": "Посмотрите на экран KeepKey и нажмите на позиции для установки нового PIN-кода", "confirmDescription": "Введите тот же PIN-код ещё раз для подтверждения", "backspace": "Назад", - "unlock": "Разблокировать" + "unlock": "Разблокировать", + "forgotPinWipe": "Забыли PIN? Сбросить устройство", + "wipeWarningTitle": "Сбросить ваш KeepKey", + "wipeWarningDescription": "Сброс вашего KeepKey удалит все ключи и настройки. Вы потеряете доступ к своим средствам, если у вас нет фразы восстановления.", + "wipeAcknowledge": "Я сохранил фразу восстановления и понимаю, что делаю", + "wipeDevice": "Сбросить устройство", + "wiping": "Сброс..." }, "passphrase": { "title": "Введите кодовую фразу", diff --git a/projects/keepkey-vault/src/mainview/i18n/locales/zh/device.json b/projects/keepkey-vault/src/mainview/i18n/locales/zh/device.json index d17d8a9..4113bce 100644 --- a/projects/keepkey-vault/src/mainview/i18n/locales/zh/device.json +++ b/projects/keepkey-vault/src/mainview/i18n/locales/zh/device.json @@ -7,7 +7,13 @@ "createDescription": "查看 KeepKey 屏幕并点击相应位置设置新 PIN", "confirmDescription": "再次输入相同的 PIN 以确认", "backspace": "退格", - "unlock": "解锁" + "unlock": "解锁", + "forgotPinWipe": "忘记 PIN?擦除设备", + "wipeWarningTitle": "擦除您的 KeepKey", + "wipeWarningDescription": "擦除您的 KeepKey 将删除所有密钥和设置。如果您没有恢复短语,将无法访问您的资金。", + "wipeAcknowledge": "我已备份恢复短语,并了解我正在做什么", + "wipeDevice": "擦除设备", + "wiping": "正在擦除..." }, "passphrase": { "title": "输入密码短语", diff --git a/projects/keepkey-vault/src/mainview/index.css b/projects/keepkey-vault/src/mainview/index.css index dbd3eee..ceba06c 100644 --- a/projects/keepkey-vault/src/mainview/index.css +++ b/projects/keepkey-vault/src/mainview/index.css @@ -12,6 +12,11 @@ html, body, #root { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } +/* Prevent native window drag from capturing form inputs */ +input, textarea, select, button, [contenteditable], a, [role="button"], [role="textbox"] { + -webkit-app-region: no-drag; +} + ::-webkit-scrollbar { width: 6px; }