Improve locked vault browser workflow

This commit is contained in:
Joe Julian
2026-04-23 20:37:49 -07:00
parent 4afbc3c933
commit d60a8d2fbf
5 changed files with 69 additions and 4 deletions
+14 -3
View File
@@ -292,6 +292,16 @@ function approvalHintForState(state) {
return state.pendingMessage || "Approve or deny the fill request in KeePassGO."; 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) { function schedulePendingPoll(tabId, pageUrl) {
if (!Number.isInteger(tabId)) { if (!Number.isInteger(tabId)) {
return; return;
@@ -492,7 +502,7 @@ async function refreshPageState(tabId, pageUrl, options = {}) {
state.matches = []; state.matches = [];
state.updatedAt = Date.now(); state.updatedAt = Date.now();
const saved = await setPageState(tabId, state); const saved = await setPageState(tabId, state);
if (saved.pendingFill) { if (shouldContinueWatchingState(saved)) {
schedulePendingPoll(tabId, resolvedURL); schedulePendingPoll(tabId, resolvedURL);
} else { } else {
clearPendingPoll(tabId); clearPendingPoll(tabId);
@@ -502,7 +512,7 @@ async function refreshPageState(tabId, pageUrl, options = {}) {
if (shouldReuseMatches(state, force)) { if (shouldReuseMatches(state, force)) {
const saved = await setPageState(tabId, state); const saved = await setPageState(tabId, state);
if (saved.pendingFill) { if (shouldContinueWatchingState(saved)) {
schedulePendingPoll(tabId, resolvedURL); schedulePendingPoll(tabId, resolvedURL);
} else { } else {
clearPendingPoll(tabId); clearPendingPoll(tabId);
@@ -529,7 +539,7 @@ async function refreshPageState(tabId, pageUrl, options = {}) {
updatedAt: Date.now() updatedAt: Date.now()
}; };
const saved = await setPageState(tabId, state); const saved = await setPageState(tabId, state);
if (saved.pendingFill) { if (shouldContinueWatchingState(saved)) {
schedulePendingPoll(tabId, resolvedURL); schedulePendingPoll(tabId, resolvedURL);
} else { } else {
clearPendingPoll(tabId); clearPendingPoll(tabId);
@@ -657,6 +667,7 @@ const backgroundTestExports = {
normalizePageState, normalizePageState,
actionPresentationForState, actionPresentationForState,
shouldReuseMatches, shouldReuseMatches,
shouldContinueWatchingState,
tokenPendingApprovalCount, tokenPendingApprovalCount,
defaultSettings defaultSettings
}; };
+18
View File
@@ -49,6 +49,24 @@ test("tokenPendingApprovalCount reads token-scoped approval state", () => {
assert.equal(background.tokenPendingApprovalCount({}), 0); 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", () => { test("default settings include a blank bearer token that can be overridden by harness patching", () => {
assert.equal(background.defaultSettings.bearerToken, ""); assert.equal(background.defaultSettings.bearerToken, "");
}); });
+5 -1
View File
@@ -429,6 +429,7 @@ function shouldShowInlineOverlay(state, hasTarget, suppressed, idleHidden) {
state?.pageHasLoginForm && state?.pageHasLoginForm &&
( (
state?.pendingFill || state?.pendingFill ||
(state?.configured && state?.success && state?.status?.locked) ||
(state?.configured && state?.success && !state?.status?.locked && Array.isArray(state?.matches) && state.matches.length > 0) (state?.configured && state?.success && !state?.status?.locked && Array.isArray(state?.matches) && state.matches.length > 0)
) )
); );
@@ -727,10 +728,13 @@ if (isNodeTestEnv) {
ensureRootMounted(); ensureRootMounted();
dock.style.display = "block"; 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) { if (pageState.pendingFill) {
meta.textContent = "Approval needed in KeePassGO"; meta.textContent = "Approval needed in KeePassGO";
panelCopy.textContent = pageState.pendingMessage || "Approve or deny the fill request 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 { } else {
const count = Array.isArray(pageState.matches) ? pageState.matches.length : 0; const count = Array.isArray(pageState.matches) ? pageState.matches.length : 0;
meta.textContent = count === 1 ? "1 login ready" : `${count} logins ready`; meta.textContent = count === 1 ? "1 login ready" : `${count} logins ready`;
+13
View File
@@ -94,6 +94,19 @@ test("shouldShowInlineOverlay hides the page overlay after it is suppressed", ()
assert.equal(content.shouldShowInlineOverlay(state, true, true, false), false); 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", () => { test("shouldShowInlineOverlay hides the page overlay after idle expiry", () => {
const state = { const state = {
pageHasLoginForm: true, pageHasLoginForm: true,
+19
View File
@@ -115,6 +115,25 @@ Expected behavior:
- custom URL fields such as `URL1`, `URL2`, and similar KeePass-style URL - custom URL fields such as `URL1`, `URL2`, and similar KeePass-style URL
slots 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: For extension-side regression checks, run:
```bash ```bash