mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-05-09 15:56:11 -04:00
Open matched credential in browser extension by default when opening popup (#1740)
This commit is contained in:
committed by
Leendert de Borst
parent
57e325f98e
commit
10a676447b
@@ -0,0 +1,89 @@
|
||||
import { useCallback } from 'react';
|
||||
import { sendMessage } from 'webext-bridge/popup';
|
||||
|
||||
import type { Item } from '@/utils/dist/core/models/vault';
|
||||
import { LocalPreferencesService } from '@/utils/LocalPreferencesService';
|
||||
import type { ItemsResponse } from '@/utils/types/messaging/ItemsResponse';
|
||||
|
||||
import { browser } from '#imports';
|
||||
|
||||
/**
|
||||
* Result of current tab matching.
|
||||
*/
|
||||
export type CurrentTabMatchResult = {
|
||||
/** Matched items for the current tab */
|
||||
items: Item[];
|
||||
/** Current tab URL (for prefilling search) */
|
||||
currentUrl: string;
|
||||
/** Current tab domain (for display/search) */
|
||||
domain: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook for matching vault items against the current browser tab.
|
||||
*
|
||||
* Uses the same credential matching logic as the content script autofill popup,
|
||||
* respecting user's autofill matching mode settings.
|
||||
*/
|
||||
const useCurrentTabMatching = (): {
|
||||
matchCurrentTab: () => Promise<CurrentTabMatchResult | null>;
|
||||
} => {
|
||||
/**
|
||||
* Match vault items against the current browser tab.
|
||||
*
|
||||
* @returns Promise resolving to match result, or null if matching fails
|
||||
*/
|
||||
const matchCurrentTab = useCallback(async (): Promise<CurrentTabMatchResult | null> => {
|
||||
try {
|
||||
// Get the current active tab
|
||||
const [activeTab] = await browser.tabs.query({ active: true, currentWindow: true });
|
||||
|
||||
if (!activeTab?.url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Skip non-http(s) URLs (like chrome://, about:, etc.)
|
||||
if (!activeTab.url.startsWith('http://') && !activeTab.url.startsWith('https://')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extract domain for search prefill
|
||||
let domain = '';
|
||||
try {
|
||||
const url = new URL(activeTab.url);
|
||||
domain = url.hostname.replace(/^www\./, '');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get autofill matching mode from user settings
|
||||
const matchingMode = await LocalPreferencesService.getAutofillMatchingMode();
|
||||
|
||||
// Use the same filtering logic as content script
|
||||
const response = await sendMessage('GET_FILTERED_ITEMS', {
|
||||
currentUrl: activeTab.url,
|
||||
pageTitle: activeTab.title || '',
|
||||
matchingMode: matchingMode
|
||||
}, 'background') as ItemsResponse;
|
||||
|
||||
if (!response.success || !response.items) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
items: response.items,
|
||||
currentUrl: activeTab.url,
|
||||
domain: domain
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error matching current tab:', error);
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
matchCurrentTab,
|
||||
};
|
||||
};
|
||||
|
||||
export default useCurrentTabMatching;
|
||||
@@ -21,20 +21,6 @@ type ServiceDetectionResult = {
|
||||
* Service detection sources (in priority order):
|
||||
* 1. URL parameters (serviceName, serviceUrl, currentUrl) - e.g., from content script popout
|
||||
* 2. Active browser tab - for dashboard/popup opened directly
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { detectService } = useServiceDetection();
|
||||
*
|
||||
* useEffect(() => {
|
||||
* const init = async () => {
|
||||
* const { serviceName, serviceUrl } = await detectService(itemNameParam);
|
||||
* setItem({ ...item, Name: serviceName });
|
||||
* setFieldValues(prev => ({ ...prev, 'login.url': serviceUrl }));
|
||||
* };
|
||||
* init();
|
||||
* }, []);
|
||||
* ```
|
||||
*/
|
||||
const useServiceDetection = (): {
|
||||
detectService: (fallbackName?: string | null) => Promise<ServiceDetectionResult>;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { sendMessage } from 'webext-bridge/popup';
|
||||
import { useApp } from '@/entrypoints/popup/context/AppContext';
|
||||
import { useDb } from '@/entrypoints/popup/context/DbContext';
|
||||
import { useLoading } from '@/entrypoints/popup/context/LoadingContext';
|
||||
import useCurrentTabMatching from '@/entrypoints/popup/hooks/useCurrentTabMatching';
|
||||
import { consumePendingRedirectUrl } from '@/entrypoints/popup/hooks/useVaultLockRedirect';
|
||||
import { useVaultSync } from '@/entrypoints/popup/hooks/useVaultSync';
|
||||
|
||||
@@ -29,6 +30,7 @@ const Reinitialize: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { setIsInitialLoading } = useLoading();
|
||||
const { syncVault } = useVaultSync();
|
||||
const { matchCurrentTab } = useCurrentTabMatching();
|
||||
const hasInitialized = useRef(false);
|
||||
|
||||
// Auth and DB state
|
||||
@@ -39,10 +41,49 @@ const Reinitialize: React.FC = () => {
|
||||
const isFullyInitialized = authInitialized && dbInitialized;
|
||||
const requiresAuth = isFullyInitialized && (!isLoggedIn || !dbAvailable);
|
||||
|
||||
/**
|
||||
* Get the expected navigation path based on URL matching result.
|
||||
*/
|
||||
const getMatchedPath = useCallback((matchResult: { items: { Id: string }[]; domain: string } | null): string => {
|
||||
if (matchResult && matchResult.items.length === 1) {
|
||||
return `/items/${matchResult.items[0].Id}`;
|
||||
} else if (matchResult && matchResult.items.length > 1) {
|
||||
/*
|
||||
* For multiple matches, we navigate to /items with search param,
|
||||
* but the saved lastPage won't have the param, so we just check against /items
|
||||
*/
|
||||
return '/items';
|
||||
} else {
|
||||
return '/items';
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Navigate based on URL matching for the current tab.
|
||||
*/
|
||||
const navigateWithUrlMatching = useCallback(async (matchResult: { items: { Id: string }[]; domain: string } | null): Promise<void> => {
|
||||
if (matchResult && matchResult.items.length === 1) {
|
||||
// Single match - navigate to items first, then to the item (for back button support)
|
||||
navigate('/items', { replace: true });
|
||||
navigate(`/items/${matchResult.items[0].Id}`, { replace: false });
|
||||
} else if (matchResult && matchResult.items.length > 1) {
|
||||
// Multiple matches - navigate to items list with domain search
|
||||
navigate(`/items?search=${encodeURIComponent(matchResult.domain)}`, { replace: true });
|
||||
} else {
|
||||
// No matches or matching failed - navigate to items page as default
|
||||
navigate('/items', { replace: true });
|
||||
}
|
||||
}, [navigate]);
|
||||
|
||||
/**
|
||||
* Restore the last visited page and navigation history if it was visited within the memory duration.
|
||||
* Compares with URL matching result - if user navigated away from matched page, restore their navigation.
|
||||
*/
|
||||
const restoreLastPage = useCallback(async (): Promise<void> => {
|
||||
// First, run URL matching to see what we would auto-navigate to
|
||||
const matchResult = await matchCurrentTab();
|
||||
const matchedPath = getMatchedPath(matchResult);
|
||||
|
||||
const [lastPage, lastVisitTime, savedHistory] = await Promise.all([
|
||||
storage.getItem(LAST_VISITED_PAGE_KEY) as Promise<string>,
|
||||
storage.getItem(LAST_VISITED_TIME_KEY) as Promise<number>,
|
||||
@@ -52,21 +93,36 @@ const Reinitialize: React.FC = () => {
|
||||
if (lastPage && lastVisitTime) {
|
||||
const timeSinceLastVisit = Date.now() - lastVisitTime;
|
||||
if (timeSinceLastVisit <= PAGE_MEMORY_DURATION) {
|
||||
// For nested routes, build up the navigation history properly
|
||||
if (savedHistory?.length > 1) {
|
||||
// Navigate to the base route first
|
||||
navigate(savedHistory[0].pathname, { replace: true });
|
||||
// Then navigate to the final destination
|
||||
navigate(lastPage, { replace: false });
|
||||
} else {
|
||||
// Simple navigation for non-nested routes
|
||||
navigate(lastPage, { replace: true });
|
||||
/*
|
||||
* Check if user navigated away from the auto-matched page to a specific different page.
|
||||
* Use fresh URL matching if:
|
||||
* - lastPage matches what URL matching would show (user stayed on auto-matched page)
|
||||
* - lastPage is /items (default index page - treat as "home" state)
|
||||
*
|
||||
* Restore user's navigation only if they navigated to a specific different page like:
|
||||
* - Settings, add/edit forms, a different item, folder view, etc.
|
||||
*/
|
||||
const isOnMatchedPage = lastPage === matchedPath;
|
||||
const isOnDefaultIndexPage = lastPage === '/items';
|
||||
const shouldUseFreshMatch = isOnMatchedPage || isOnDefaultIndexPage;
|
||||
|
||||
if (!shouldUseFreshMatch) {
|
||||
// Restore user's navigation since they navigated away from auto-matched page
|
||||
if (savedHistory?.length > 1) {
|
||||
// Navigate to the base route first
|
||||
navigate(savedHistory[0].pathname, { replace: true });
|
||||
// Then navigate to the final destination
|
||||
navigate(lastPage, { replace: false });
|
||||
} else {
|
||||
// Simple navigation for non-nested routes
|
||||
navigate(lastPage, { replace: true });
|
||||
}
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Duration has expired, clear all stored navigation data
|
||||
// Clear stored navigation data since we're using fresh URL matching
|
||||
await Promise.all([
|
||||
storage.removeItem(LAST_VISITED_PAGE_KEY),
|
||||
storage.removeItem(LAST_VISITED_TIME_KEY),
|
||||
@@ -74,9 +130,9 @@ const Reinitialize: React.FC = () => {
|
||||
sendMessage('CLEAR_PERSISTED_FORM_VALUES', null, 'background'),
|
||||
]);
|
||||
|
||||
// Navigate to the items page as default entry page
|
||||
navigate('/items', { replace: true });
|
||||
}, [navigate]);
|
||||
// Navigate based on URL matching
|
||||
await navigateWithUrlMatching(matchResult);
|
||||
}, [navigate, matchCurrentTab, getMatchedPath, navigateWithUrlMatching]);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import { useLocation, useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import DeleteFolderModal from '@/entrypoints/popup/components/Folders/DeleteFolderModal';
|
||||
import FolderModal from '@/entrypoints/popup/components/Folders/FolderModal';
|
||||
@@ -110,6 +110,7 @@ const ItemsList: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { folderId: folderIdParam } = useParams<{ folderId?: string }>();
|
||||
const location = useLocation();
|
||||
const [searchParams] = useSearchParams();
|
||||
const dbContext = useDb();
|
||||
const app = useApp();
|
||||
const navigate = useNavigate();
|
||||
@@ -117,7 +118,8 @@ const ItemsList: React.FC = () => {
|
||||
const { executeVaultMutationAsync } = useVaultMutate();
|
||||
const { setHeaderButtons } = useHeaderButtons();
|
||||
const [items, setItems] = useState<Item[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
// Initialize searchTerm from URL query parameter if present
|
||||
const [searchTerm, setSearchTerm] = useState(() => searchParams.get('search') || '');
|
||||
const [filterType, setFilterType] = useState<FilterType>(getStoredFilter());
|
||||
const [showFilterMenu, setShowFilterMenu] = useState(false);
|
||||
const [showFolderModal, setShowFolderModal] = useState(false);
|
||||
@@ -155,6 +157,17 @@ const ItemsList: React.FC = () => {
|
||||
*/
|
||||
const [isLoading, setIsLoading] = useMinDurationLoading(true, 100);
|
||||
|
||||
/**
|
||||
* Clear search param from URL after reading it (to prevent it from persisting in browser history).
|
||||
*/
|
||||
useEffect(() => {
|
||||
const searchFromUrl = searchParams.get('search');
|
||||
if (searchFromUrl) {
|
||||
// Remove the search param from URL while preserving the current path
|
||||
navigate(location.pathname, { replace: true });
|
||||
}
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps -- Only run once on mount
|
||||
|
||||
/**
|
||||
* Reset search and filter when navigating via the vault tab (with resetFilters state).
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user