diff --git a/apps/browser-extension/src/entrypoints/background/VaultMessageHandler.ts b/apps/browser-extension/src/entrypoints/background/VaultMessageHandler.ts index 9424898f8..5b6504bfa 100644 --- a/apps/browser-extension/src/entrypoints/background/VaultMessageHandler.ts +++ b/apps/browser-extension/src/entrypoints/background/VaultMessageHandler.ts @@ -14,9 +14,9 @@ import { VaultUploadResponse as messageVaultUploadResponse } from '@/utils/types import { WebApiService } from '@/utils/WebApiService'; /** - * Check if the user is logged in and if the vault is locked. + * Check if the user is logged in and if the vault is locked, and also check for pending migrations. */ -export async function handleCheckAuthStatus() : Promise<{ isLoggedIn: boolean, isVaultLocked: boolean }> { +export async function handleCheckAuthStatus() : Promise<{ isLoggedIn: boolean, isVaultLocked: boolean, hasPendingMigrations: boolean, error?: string }> { const username = await storage.getItem('local:username'); const accessToken = await storage.getItem('local:accessToken'); const vaultData = await storage.getItem('session:encryptedVault'); @@ -24,10 +24,42 @@ export async function handleCheckAuthStatus() : Promise<{ isLoggedIn: boolean, i const isLoggedIn = username !== null && accessToken !== null; const isVaultLocked = isLoggedIn && vaultData === null; - return { - isLoggedIn, - isVaultLocked - }; + // If vault is locked, we can't check for pending migrations + if (isVaultLocked) { + return { + isLoggedIn, + isVaultLocked, + hasPendingMigrations: false + }; + } + + // If not logged in, no need to check migrations + if (!isLoggedIn) { + return { + isLoggedIn, + isVaultLocked, + hasPendingMigrations: false + }; + } + + // Vault is unlocked, check for pending migrations + try { + const sqliteClient = await createVaultSqliteClient(); + const hasPendingMigrations = await sqliteClient.hasPendingMigrations(); + return { + isLoggedIn, + isVaultLocked, + hasPendingMigrations + }; + } catch (error) { + console.error('Error checking pending migrations:', error); + return { + isLoggedIn, + isVaultLocked, + hasPendingMigrations: false, + error: error instanceof Error ? error.message : 'An unknown error occurred' + }; + } } /** diff --git a/apps/browser-extension/src/entrypoints/content.ts b/apps/browser-extension/src/entrypoints/content.ts index c19310738..328245b0f 100644 --- a/apps/browser-extension/src/entrypoints/content.ts +++ b/apps/browser-extension/src/entrypoints/content.ts @@ -2,7 +2,7 @@ import '@/entrypoints/contentScript/style.css'; import { onMessage } from "webext-bridge/content-script"; import { injectIcon, popupDebounceTimeHasPassed, validateInputField } from '@/entrypoints/contentScript/Form'; -import { isAutoShowPopupEnabled, openAutofillPopup, removeExistingPopup } from '@/entrypoints/contentScript/Popup'; +import { isAutoShowPopupEnabled, openAutofillPopup, removeExistingPopup, createUpgradeRequiredPopup } from '@/entrypoints/contentScript/Popup'; import { FormDetector } from '@/utils/formDetector/FormDetector'; import { BoolResponse as messageBoolResponse } from '@/utils/types/messaging/BoolResponse'; @@ -69,7 +69,7 @@ export default defineContentScript({ // Only show popup if debounce time has passed if (popupDebounceTimeHasPassed()) { - openAutofillPopup(inputElement, container); + await showPopupWithAuthCheck(inputElement, container); } } } @@ -132,6 +132,48 @@ export default defineContentScript({ if (canShowPopup) { injectIcon(inputElement, container); + await showPopupWithAuthCheck(inputElement, container); + } + } + + /** + * Show popup with auth check. + */ + async function showPopupWithAuthCheck(inputElement: HTMLInputElement, container: HTMLElement) : Promise { + try { + // Check auth status and pending migrations in a single call + const { sendMessage } = await import('webext-bridge/content-script'); + const authStatus = await sendMessage('CHECK_AUTH_STATUS', {}, 'background') as { + isLoggedIn: boolean, + isVaultLocked: boolean, + hasPendingMigrations: boolean, + error?: string + }; + + if (authStatus.isVaultLocked) { + // Vault is locked, show vault locked popup + const { createVaultLockedPopup } = await import('@/entrypoints/contentScript/Popup'); + createVaultLockedPopup(inputElement, container); + return; + } + + if (authStatus.hasPendingMigrations) { + // Show upgrade required popup + createUpgradeRequiredPopup(inputElement, container, 'Vault upgrade required.'); + return; + } + + if (authStatus.error) { + // Show upgrade required popup for version-related errors + createUpgradeRequiredPopup(inputElement, container, authStatus.error); + return; + } + + // No upgrade required, show normal autofill popup + openAutofillPopup(inputElement, container); + } catch (error) { + console.error('Error checking vault status:', error); + // Fall back to normal autofill popup if check fails openAutofillPopup(inputElement, container); } } diff --git a/apps/browser-extension/src/entrypoints/contentScript/Popup.ts b/apps/browser-extension/src/entrypoints/contentScript/Popup.ts index 926874176..3d533bb41 100644 --- a/apps/browser-extension/src/entrypoints/contentScript/Popup.ts +++ b/apps/browser-extension/src/entrypoints/contentScript/Popup.ts @@ -1462,3 +1462,92 @@ function addReliableClickHandler(element: HTMLElement, handler: (e: Event) => vo isMouseDown = false; }, { capture: true }); } + +/** + * Create upgrade required popup. + */ +export function createUpgradeRequiredPopup(input: HTMLInputElement, rootContainer: HTMLElement, errorMessage: string): void { + /** + * Handle upgrade click. + */ + const handleUpgradeClick = () : void => { + sendMessage('OPEN_POPUP', {}, 'background'); + removeExistingPopup(rootContainer); + } + + const popup = createBasePopup(input, rootContainer); + popup.classList.add('av-upgrade-required'); + + // Create container for message and button + const container = document.createElement('div'); + container.className = 'av-upgrade-required-container'; + + // Make the entire container clickable + addReliableClickHandler(container, handleUpgradeClick); + container.style.cursor = 'pointer'; + + // Add message + const messageElement = document.createElement('div'); + messageElement.className = 'av-upgrade-required-message'; + messageElement.textContent = errorMessage; + container.appendChild(messageElement); + + // Add upgrade button with SVG icon + const button = document.createElement('button'); + button.title = 'Open AliasVault to upgrade'; + button.className = 'av-upgrade-required-button'; + button.innerHTML = ` + + + + `; + 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-upgrade-required-close'; + closeButton.title = 'Dismiss popup'; + closeButton.innerHTML = ` + + + + + `; + + // 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 + addReliableClickHandler(closeButton, (e) => { + e.stopPropagation(); // Prevent opening the upgrade popup + removeExistingPopup(rootContainer); + }); + + popup.appendChild(closeButton); + + /** + * Add event listener to document to close popup when clicking outside. + */ + const handleClickOutside = (event: MouseEvent): void => { + const target = event.target as Node; + const targetElement = event.target as HTMLElement; + + // Check if the click is outside the popup and outside the shadow UI + if (popup && !popup.contains(target) && !input.contains(target) && targetElement.tagName !== 'ALIASVAULT-UI') { + removeExistingPopup(rootContainer); + document.removeEventListener('mousedown', handleClickOutside); + } + }; + + setTimeout(() => { + document.addEventListener('mousedown', handleClickOutside); + }, 100); + + rootContainer.appendChild(popup); +} diff --git a/apps/browser-extension/src/entrypoints/contentScript/style.css b/apps/browser-extension/src/entrypoints/contentScript/style.css index a35d6acd9..cf6eebb51 100644 --- a/apps/browser-extension/src/entrypoints/contentScript/style.css +++ b/apps/browser-extension/src/entrypoints/contentScript/style.css @@ -299,6 +299,71 @@ body { border: 1px solid #6f6f6f; } +/* Upgrade Required Popup */ +.av-upgrade-required { + padding: 12px 16px; + position: relative; +} + +.av-upgrade-required:hover { + background-color: #374151; +} + +.av-upgrade-required-container { + display: flex; + align-items: center; + padding-right: 32px; + width: 100%; + transition: background-color 0.2s ease; + border-radius: 4px; +} + +.av-upgrade-required-message { + color: #d1d5db; + font-size: 14px; + flex-grow: 1; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; +} + +.av-upgrade-required-button { + background: none; + border: none; + cursor: pointer; + padding: 4px; + padding-right: 28px; + display: flex; + align-items: center; + justify-content: center; + color: #f59e0b; + border-radius: 4px; + margin-left: 8px; +} + +.av-upgrade-required-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; +} + +.av-icon-upgrade { + width: 16px; + height: 16px; + fill: none; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} + /* Create Name Popup */ .av-create-popup-overlay { position: fixed;