From d60a8d2fbf3369c457074edea6ce672b4acb9291 Mon Sep 17 00:00:00 2001 From: Joe Julian Date: Thu, 23 Apr 2026 20:37:49 -0700 Subject: [PATCH] Improve locked vault browser workflow --- browser/extension/background.js | 17 ++++++++++++++--- browser/extension/background.test.cjs | 18 ++++++++++++++++++ browser/extension/content.js | 6 +++++- browser/extension/content.test.cjs | 13 +++++++++++++ docs/browser-extension.md | 19 +++++++++++++++++++ 5 files changed, 69 insertions(+), 4 deletions(-) diff --git a/browser/extension/background.js b/browser/extension/background.js index fcaf63b..a0840ac 100644 --- a/browser/extension/background.js +++ b/browser/extension/background.js @@ -292,6 +292,16 @@ function approvalHintForState(state) { return state.pendingMessage || "Approve or deny the fill request in KeePassGO."; } +function shouldContinueWatchingState(state) { + if (!state?.pageHasLoginForm) { + return false; + } + if (state?.pendingFill) { + return true; + } + return Boolean(state?.status?.locked); +} + function schedulePendingPoll(tabId, pageUrl) { if (!Number.isInteger(tabId)) { return; @@ -492,7 +502,7 @@ async function refreshPageState(tabId, pageUrl, options = {}) { state.matches = []; state.updatedAt = Date.now(); const saved = await setPageState(tabId, state); - if (saved.pendingFill) { + if (shouldContinueWatchingState(saved)) { schedulePendingPoll(tabId, resolvedURL); } else { clearPendingPoll(tabId); @@ -502,7 +512,7 @@ async function refreshPageState(tabId, pageUrl, options = {}) { if (shouldReuseMatches(state, force)) { const saved = await setPageState(tabId, state); - if (saved.pendingFill) { + if (shouldContinueWatchingState(saved)) { schedulePendingPoll(tabId, resolvedURL); } else { clearPendingPoll(tabId); @@ -529,7 +539,7 @@ async function refreshPageState(tabId, pageUrl, options = {}) { updatedAt: Date.now() }; const saved = await setPageState(tabId, state); - if (saved.pendingFill) { + if (shouldContinueWatchingState(saved)) { schedulePendingPoll(tabId, resolvedURL); } else { clearPendingPoll(tabId); @@ -657,6 +667,7 @@ const backgroundTestExports = { normalizePageState, actionPresentationForState, shouldReuseMatches, + shouldContinueWatchingState, tokenPendingApprovalCount, defaultSettings }; diff --git a/browser/extension/background.test.cjs b/browser/extension/background.test.cjs index c6d7e4e..77db9a7 100644 --- a/browser/extension/background.test.cjs +++ b/browser/extension/background.test.cjs @@ -49,6 +49,24 @@ test("tokenPendingApprovalCount reads token-scoped approval state", () => { assert.equal(background.tokenPendingApprovalCount({}), 0); }); +test("shouldContinueWatchingState keeps polling locked login pages", () => { + assert.equal(background.shouldContinueWatchingState({ + pageHasLoginForm: true, + pendingFill: false, + status: { locked: true } + }), true); + assert.equal(background.shouldContinueWatchingState({ + pageHasLoginForm: true, + pendingFill: true, + status: { locked: false } + }), true); + assert.equal(background.shouldContinueWatchingState({ + pageHasLoginForm: true, + pendingFill: false, + status: { locked: false } + }), false); +}); + test("default settings include a blank bearer token that can be overridden by harness patching", () => { assert.equal(background.defaultSettings.bearerToken, ""); }); diff --git a/browser/extension/content.js b/browser/extension/content.js index cc09521..5b87599 100644 --- a/browser/extension/content.js +++ b/browser/extension/content.js @@ -429,6 +429,7 @@ function shouldShowInlineOverlay(state, hasTarget, suppressed, idleHidden) { state?.pageHasLoginForm && ( state?.pendingFill || + (state?.configured && state?.success && state?.status?.locked) || (state?.configured && state?.success && !state?.status?.locked && Array.isArray(state?.matches) && state.matches.length > 0) ) ); @@ -727,10 +728,13 @@ if (isNodeTestEnv) { ensureRootMounted(); dock.style.display = "block"; - trigger.dataset.tone = pageState.pendingFill ? "warning" : (pageState.error ? "error" : "ready"); + trigger.dataset.tone = pageState.pendingFill || pageState.status?.locked ? "warning" : (pageState.error ? "error" : "ready"); if (pageState.pendingFill) { meta.textContent = "Approval needed in KeePassGO"; panelCopy.textContent = pageState.pendingMessage || "Approve or deny the fill request in KeePassGO."; + } else if (pageState.status?.locked) { + meta.textContent = "Unlock KeePassGO"; + panelCopy.textContent = "Unlock KeePassGO to turn this field back into live login suggestions."; } else { const count = Array.isArray(pageState.matches) ? pageState.matches.length : 0; meta.textContent = count === 1 ? "1 login ready" : `${count} logins ready`; diff --git a/browser/extension/content.test.cjs b/browser/extension/content.test.cjs index ecee94d..e18d13c 100644 --- a/browser/extension/content.test.cjs +++ b/browser/extension/content.test.cjs @@ -94,6 +94,19 @@ test("shouldShowInlineOverlay hides the page overlay after it is suppressed", () assert.equal(content.shouldShowInlineOverlay(state, true, true, false), false); }); +test("shouldShowInlineOverlay stays visible for locked login pages", () => { + const state = { + pageHasLoginForm: true, + configured: true, + success: true, + status: { locked: true }, + matches: [], + pendingFill: false + }; + + assert.equal(content.shouldShowInlineOverlay(state, true, false, false), true); +}); + test("shouldShowInlineOverlay hides the page overlay after idle expiry", () => { const state = { pageHasLoginForm: true, diff --git a/docs/browser-extension.md b/docs/browser-extension.md index 87413e7..2e47f7c 100644 --- a/docs/browser-extension.md +++ b/docs/browser-extension.md @@ -115,6 +115,25 @@ Expected behavior: - custom URL fields such as `URL1`, `URL2`, and similar KeePass-style URL slots +## Locked Vault Workflow + +User story: + +- When the current page has a login form but KeePassGO is locked, the browser + must still make that state visible on the page and in the popup. +- Unlocking KeePassGO should not require the user to reopen the popup multiple + times or reload the page before the extension becomes usable again. + +Expected behavior: + +- The popup shows a locked-state message instead of silently falling back to + "no matches." +- The inline page affordance stays visible on login forms while KeePassGO is + locked and tells the user to unlock the vault. +- After the vault is unlocked, the extension rechecks the page automatically + and turns the locked affordance back into live matches without requiring a + page reload. + For extension-side regression checks, run: ```bash