Show autofill popup dismiss button when vault is locked (#682)

This commit is contained in:
Leendert de Borst
2025-03-13 13:31:04 +01:00
committed by Leendert de Borst
parent 852d9b5e98
commit 4fdf7ce92c
10 changed files with 193 additions and 40 deletions

View File

@@ -2,7 +2,7 @@ import { browser } from "wxt/browser";
import { defineBackground } from 'wxt/sandbox';
import { onMessage } from "webext-bridge/background";
import { setupContextMenus, handleContextMenuClick } from './background/ContextMenu';
import { handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetDefaultEmailDomain, handleGetDerivedKey, handleGetVault, handleStoreVault, handleSyncVault } from './background/VaultMessageHandler';
import { handleCheckAuthStatus, handleClearVault, handleCreateIdentity, handleGetCredentials, handleGetDefaultEmailDomain, handleGetDerivedKey, handleGetVault, handleStoreVault, handleSyncVault } from './background/VaultMessageHandler';
import { handleOpenPopup, handlePopupWithCredential } from './background/PopupMessageHandler';
export default defineBackground({
@@ -17,6 +17,7 @@ export default defineBackground({
);
// Listen for messages using webext-bridge
onMessage('CHECK_AUTH_STATUS', () => handleCheckAuthStatus());
onMessage('STORE_VAULT', ({ data }) => handleStoreVault(data));
onMessage('SYNC_VAULT', () => handleSyncVault());
onMessage('GET_VAULT', () => handleGetVault());

View File

@@ -11,6 +11,23 @@ import { VaultResponse as messageVaultResponse } from '../../utils/types/messagi
import { CredentialsResponse as messageCredentialsResponse } from '../../utils/types/messaging/CredentialsResponse';
import { DefaultEmailDomainResponse as messageDefaultEmailDomainResponse } from '../../utils/types/messaging/DefaultEmailDomainResponse';
/**
* Check if the user is logged in and if the vault is locked.
*/
export async function handleCheckAuthStatus() : Promise<{ isLoggedIn: boolean, isVaultLocked: boolean }> {
const username = await storage.getItem('local:username');
const accessToken = await storage.getItem('local:accessToken');
const vaultData = await storage.getItem('session:encryptedVault');
const isLoggedIn = username !== null && accessToken !== null;
const isVaultLocked = isLoggedIn && vaultData !== null;
return {
isLoggedIn,
isVaultLocked
};
}
/**
* Store the vault in browser storage.
*/

View File

@@ -1,7 +1,7 @@
import './contentScript/style.css';
import { FormDetector } from '../utils/formDetector/FormDetector';
import { isAutoShowPopupDisabled, openAutofillPopup, removeExistingPopup } from './contentScript/Popup';
import { canShowPopup, injectIcon } from './contentScript/Form';
import { isAutoShowPopupEnabled, openAutofillPopup, removeExistingPopup } from './contentScript/Popup';
import { injectIcon, popupDebounceTimeHasPassed } from './contentScript/Form';
import { onMessage } from "webext-bridge/content-script";
import { BoolResponse as messageBoolResponse } from '../utils/types/messaging/BoolResponse';
import { defineContentScript } from 'wxt/sandbox';
@@ -42,22 +42,16 @@ export default defineContentScript({
const target = e.target as HTMLInputElement;
const textInputTypes = ['text', 'email', 'tel', 'password', 'search', 'url'];
if (target.tagName === 'INPUT' &&
textInputTypes.includes(target.type) &&
!target.dataset.aliasvaultIgnore) {
if (target.tagName === 'INPUT' && textInputTypes.includes(target.type) && !target.dataset.aliasvaultIgnore) {
const formDetector = new FormDetector(document, target);
if (!formDetector.containsLoginForm()) {
return;
}
injectIcon(target, container);
const isDisabled = await isAutoShowPopupDisabled();
const canShow = canShowPopup();
// Only show popup if it's not disabled and the popup can be shown
if (!isDisabled && canShow) {
// Only show popup if its enabled and debounce time has passed.
if (await isAutoShowPopupEnabled() && popupDebounceTimeHasPassed()) {
openAutofillPopup(target, container);
}
}

View File

@@ -14,7 +14,7 @@ let popupDebounceTime = 0;
/**
* Check if popup can be shown based on debounce time.
*/
export function canShowPopup() : boolean {
export function popupDebounceTimeHasPassed() : boolean {
if (Date.now() < popupDebounceTime) {
return false;
}

View File

@@ -443,19 +443,25 @@ export function createAutofillPopup(input: HTMLInputElement, credentials: Creden
* Create vault locked popup.
*/
export function createVaultLockedPopup(input: HTMLInputElement, rootContainer: HTMLElement): void {
const popup = createBasePopup(input, rootContainer);
popup.classList.add('av-vault-locked');
// Make the whole popup clickable to open the main extension login popup.
popup.addEventListener('click', () => {
/**
* Handle unlock click.
*/
const handleUnlockClick = () : void => {
sendMessage('OPEN_POPUP', {}, 'background');
removeExistingPopup(rootContainer);
});
}
const popup = createBasePopup(input, rootContainer);
popup.classList.add('av-vault-locked');
// Create container for message and button
const container = document.createElement('div');
container.className = 'av-vault-locked-container';
// Make the entire container clickable
container.addEventListener('click', handleUnlockClick);
container.style.cursor = 'pointer';
// Add message
const messageElement = document.createElement('div');
messageElement.className = 'av-vault-locked-message';
@@ -472,10 +478,37 @@ export function createVaultLockedPopup(input: HTMLInputElement, rootContainer: H
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
`;
container.appendChild(button);
// Add the container to the popup
popup.appendChild(container);
// Add close button as a separate element positioned to the right
const closeButton = document.createElement('button');
closeButton.className = 'av-button av-button-close av-vault-locked-close';
closeButton.title = 'Dismiss popup';
closeButton.innerHTML = `
<svg class="av-icon" viewBox="0 0 24 24">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
`;
// Position the close button to the right of the container
closeButton.style.position = 'absolute';
closeButton.style.right = '8px';
closeButton.style.top = '50%';
closeButton.style.transform = 'translateY(-50%)';
// Handle close button click
closeButton.addEventListener('click', async (e) => {
e.stopPropagation(); // Prevent opening the unlock popup
await dismissVaultLockedPopup();
removeExistingPopup(rootContainer);
});
popup.appendChild(closeButton);
/**
* Add event listener to document to close popup when clicking outside.
*/
@@ -600,17 +633,35 @@ function createCredentialList(credentials: Credential[], input: HTMLInputElement
export const DISABLED_SITES_KEY = 'local:aliasvault_disabled_sites';
export const GLOBAL_POPUP_ENABLED_KEY = 'local:aliasvault_global_popup_enabled';
export const VAULT_LOCKED_DISMISS_UNTIL_KEY = 'local:aliasvault_vault_locked_dismiss_until';
/**
* Check if auto-popup is disabled for current site
*/
export async function isAutoShowPopupDisabled(): Promise<boolean> {
export async function isAutoShowPopupEnabled(): Promise<boolean> {
const disabledSites = await storage.getItem(DISABLED_SITES_KEY) as string[] ?? [];
const globalPopupEnabled = await storage.getItem(GLOBAL_POPUP_ENABLED_KEY) ?? true;
const currentHostname = window.location.hostname;
return !globalPopupEnabled || disabledSites.includes(currentHostname);
if (!globalPopupEnabled) {
// Popup is disabled for all sites.
return false;
}
if (disabledSites.includes(currentHostname)) {
// Popup is disabled for current site.
return false;
}
// Check time-based dismissal
const dismissUntil = await storage.getItem(VAULT_LOCKED_DISMISS_UNTIL_KEY) as number;
if (dismissUntil && Date.now() < dismissUntil) {
// Popup is dismissed for a certain amount of time.
return false;
}
return true;
}
/**
@@ -926,3 +977,22 @@ function detectMimeType(bytes: Uint8Array): string {
return 'image/x-icon';
}
/**
* Dismiss vault locked popup for 4 hours if user is logged in but vault is locked,
* or for 3 days if user is not logged in.
*/
export async function dismissVaultLockedPopup(): Promise<void> {
// First check if user is logged in but vault is locked, or not logged in at all
const authStatus = await sendMessage('CHECK_AUTH_STATUS', {}, 'background') as { isLoggedIn: boolean, isVaultLocked: boolean };
if (authStatus.isLoggedIn && authStatus.isVaultLocked) {
// User is logged in but vault is locked - dismiss for 4 hours
const fourHoursFromNow = Date.now() + (4 * 60 * 60 * 1000);
await storage.setItem(VAULT_LOCKED_DISMISS_UNTIL_KEY, fourHoursFromNow);
} else if (!authStatus.isLoggedIn) {
// User is not logged in - dismiss for 3 days
const threeDaysFromNow = Date.now() + (3 * 24 * 60 * 60 * 1000);
await storage.setItem(VAULT_LOCKED_DISMISS_UNTIL_KEY, threeDaysFromNow);
}
}

View File

@@ -79,7 +79,7 @@ body {
padding: 10px 16px;
border-radius: 4px;
transition: background-color 0.2s ease;
min-width: 0; /* Enable text truncation */
min-width: 0;
}
.av-credential-logo {
@@ -91,8 +91,8 @@ body {
display: flex;
flex-direction: column;
flex-grow: 1;
min-width: 0; /* Enable text truncation */
margin-right: 8px; /* Add space between text and popout icon */
min-width: 0;
margin-right: 8px;
}
.av-service-name {
@@ -121,7 +121,7 @@ body {
margin-right: 16px;
opacity: 0.6;
border-radius: 4px;
flex-shrink: 0; /* Prevent icon from shrinking */
flex-shrink: 0;
color: #ffffff;
transition: opacity 0.2s ease, background-color 0.2s ease, color 0.2s ease;
}
@@ -209,7 +209,6 @@ body {
outline: none;
line-height: 1;
text-align: center;
padding: 6px 12px;
}
.av-search-input::placeholder {
@@ -224,7 +223,7 @@ body {
/* Vault Locked Popup */
.av-vault-locked {
padding: 12px 16px;
cursor: pointer;
position: relative;
}
.av-vault-locked:hover {
@@ -234,28 +233,46 @@ body {
.av-vault-locked-container {
display: flex;
align-items: center;
position: relative;
padding-right: 32px;
width: 100%;
transition: background-color 0.2s ease;
border-radius: 4px;
}
.av-vault-locked-message {
color: #d1d5db;
font-size: 14px;
padding-right: 32px;
flex-grow: 1;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
.av-vault-locked-button {
position: absolute;
right: 0;
background: none;
border: none;
cursor: pointer;
padding: 4px;
padding-right: 28px;
display: flex;
align-items: center;
justify-content: center;
color: #d68338;
border-radius: 4px;
margin-left: 8px;
}
.av-vault-locked-close {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
padding: 4px;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
border: 1px solid #6f6f6f;
}
/* Create Name Popup */
@@ -404,4 +421,4 @@ body {
@keyframes fadeOut {
0% { opacity: 1; transform: scale(1.02); }
100% { opacity: 0; transform: scale(1); }
}
}

View File

@@ -2,6 +2,7 @@ import React, { createContext, useContext, useState, useEffect, useMemo, useCall
import { useDb } from './DbContext';
import { storage } from 'wxt/storage';
import { sendMessage } from 'webext-bridge/popup';
import { VAULT_LOCKED_DISMISS_UNTIL_KEY } from '@/entrypoints/contentScript/Popup';
type AuthContextType = {
isLoggedIn: boolean;
@@ -66,6 +67,9 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
*/
const login = useCallback(async () : Promise<void> => {
setIsLoggedIn(true);
// Clear dismiss until (which can be enabled after user has dimissed vault is locked popup) to ensure popup is shown.
await storage.setItem(VAULT_LOCKED_DISMISS_UNTIL_KEY, 0);
}, []);
/**

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react';
import { AppInfo } from '../../../utils/AppInfo';
import { storage } from 'wxt/storage';
import { GLOBAL_POPUP_ENABLED_KEY, DISABLED_SITES_KEY, VAULT_LOCKED_DISMISS_UNTIL_KEY } from '../../contentScript/Popup';
type ApiOption = {
label: string;
@@ -19,6 +20,7 @@ const AuthSettings: React.FC = () => {
const [selectedOption, setSelectedOption] = useState<string>('');
const [customUrl, setCustomUrl] = useState<string>('');
const [customClientUrl, setCustomClientUrl] = useState<string>('');
const [isGloballyEnabled, setIsGloballyEnabled] = useState<boolean>(true);
useEffect(() => {
/**
@@ -27,6 +29,15 @@ const AuthSettings: React.FC = () => {
const loadStoredSettings = async () : Promise<void> => {
const apiUrl = await storage.getItem('local:apiUrl') as string;
const clientUrl = await storage.getItem('local:clientUrl') as string;
const globallyEnabled = await storage.getItem(GLOBAL_POPUP_ENABLED_KEY) !== false; // Default to true if not set
const dismissUntil = await storage.getItem(VAULT_LOCKED_DISMISS_UNTIL_KEY) as number;
if (dismissUntil) {
setIsGloballyEnabled(false);
} else {
setIsGloballyEnabled(globallyEnabled);
}
const matchingOption = DEFAULT_OPTIONS.find(opt => opt.value === apiUrl);
if (matchingOption) {
@@ -74,6 +85,23 @@ const AuthSettings: React.FC = () => {
await storage.setItem('local:clientUrl', value);
};
/**
* Toggle global popup.
*/
const toggleGlobalPopup = async () : Promise<void> => {
const newGloballyEnabled = !isGloballyEnabled;
await storage.setItem(GLOBAL_POPUP_ENABLED_KEY, newGloballyEnabled);
if (newGloballyEnabled) {
// Reset all disabled sites when enabling globally
await storage.setItem(DISABLED_SITES_KEY, []);
await storage.setItem(VAULT_LOCKED_DISMISS_UNTIL_KEY, 0);
}
setIsGloballyEnabled(newGloballyEnabled);
};
return (
<div className="p-4">
<div className="mb-6">
@@ -124,6 +152,23 @@ const AuthSettings: React.FC = () => {
</>
)}
{/* Autofill Popup Settings Section */}
<div className="mb-6">
<div className="flex flex-col gap-2">
<p className="text-sm font-medium text-gray-900 dark:text-white">Autofill popup</p>
<button
onClick={toggleGlobalPopup}
className={`px-4 py-2 rounded-md transition-colors ${
isGloballyEnabled
? 'bg-green-200 text-green-800 hover:bg-green-300 dark:bg-green-900/30 dark:text-green-400 dark:hover:bg-green-900/50'
: 'bg-red-200 text-red-800 hover:bg-red-300 dark:bg-red-900/30 dark:text-red-400 dark:hover:bg-red-900/50'
}`}
>
{isGloballyEnabled ? 'Enabled' : 'Disabled'}
</button>
</div>
</div>
<div className="text-center text-gray-400 dark:text-gray-600">
Version: {AppInfo.VERSION}
</div>

View File

@@ -144,11 +144,11 @@ const Settings: React.FC = () => {
onClick={toggleGlobalPopup}
className={`px-4 py-2 rounded-md transition-colors ${
settings.isGloballyEnabled
? 'bg-red-500 hover:bg-red-600 text-white'
: 'bg-green-500 hover:bg-green-600 text-white'
? 'bg-green-500 hover:bg-green-600 text-white'
: 'bg-red-500 hover:bg-red-600 text-white'
}`}
>
{settings.isGloballyEnabled ? 'Disable' : 'Enable'}
{settings.isGloballyEnabled ? 'Enabled' : 'Disabled'}
</button>
</div>
</div>
@@ -164,18 +164,18 @@ const Settings: React.FC = () => {
<div>
<p className="text-sm font-medium text-gray-900 dark:text-white">Open popup on: {settings.currentUrl}</p>
<p className={`text-sm mt-1 ${settings.isEnabled ? 'text-gray-600 dark:text-gray-400' : 'text-red-600 dark:text-red-400'}`}>
{settings.isEnabled ? 'Popup is active' : 'Popup is disabled'}
{settings.isEnabled ? 'Enabled for this site' : 'Disabled for this site'}
</p>
</div>
<button
onClick={toggleCurrentSite}
className={`px-4 py-2 rounded-md transition-colors ${
settings.isEnabled
? 'bg-red-500 hover:bg-red-600 text-white'
: 'bg-green-500 hover:bg-green-600 text-white'
? 'bg-green-500 hover:bg-green-600 text-white'
: 'bg-red-500 hover:bg-red-600 text-white'
}`}
>
{settings.isEnabled ? 'Disable' : 'Enable'}
{settings.isEnabled ? 'Enabled' : 'Disabled'}
</button>
</div>

View File

@@ -9,6 +9,8 @@ import SrpUtility from '../utils/SrpUtility';
import { VaultResponse } from '../../../utils/types/webapi/VaultResponse';
import { useLoading } from '../context/LoadingContext';
import { useNavigate } from 'react-router-dom';
import { VAULT_LOCKED_DISMISS_UNTIL_KEY } from '@/entrypoints/contentScript/Popup';
import { storage } from 'wxt/storage';
/**
* Unlock page
@@ -75,6 +77,9 @@ const Unlock: React.FC = () => {
// Initialize the SQLite context with the new vault data.
await dbContext.initializeDatabase(vaultResponseJson, passwordHashBase64);
// Clear dismiss until (which can be enabled after user has dimissed vault is locked popup) to ensure popup is shown.
await storage.setItem(VAULT_LOCKED_DISMISS_UNTIL_KEY, 0);
} catch (err) {
setError('Failed to unlock vault. Please check your password and try again.');
console.error('Unlock error:', err);