diff --git a/apps/browser-extension/src/entrypoints/popup/pages/credentials/CredentialsList.tsx b/apps/browser-extension/src/entrypoints/popup/pages/credentials/CredentialsList.tsx index 704031f70..ef24b50a3 100644 --- a/apps/browser-extension/src/entrypoints/popup/pages/credentials/CredentialsList.tsx +++ b/apps/browser-extension/src/entrypoints/popup/pages/credentials/CredentialsList.tsx @@ -18,6 +18,48 @@ import type { Credential } from '@/utils/dist/shared/models/vault'; import { useMinDurationLoading } from '@/hooks/useMinDurationLoading'; +type FilterType = 'all' | 'passkeys' | 'aliases' | 'userpass'; + +const FILTER_STORAGE_KEY = 'credentials-filter'; +const FILTER_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes + +/** + * Get stored filter from localStorage if not expired + */ +const getStoredFilter = (): FilterType => { + try { + const stored = localStorage.getItem(FILTER_STORAGE_KEY); + if (!stored) return 'all'; + + const { filter, timestamp } = JSON.parse(stored); + const now = Date.now(); + + // Check if expired (5 minutes) + if (now - timestamp > FILTER_EXPIRY_MS) { + localStorage.removeItem(FILTER_STORAGE_KEY); + return 'all'; + } + + return filter as FilterType; + } catch { + return 'all'; + } +}; + +/** + * Store filter in localStorage with timestamp + */ +const storeFilter = (filter: FilterType): void => { + try { + localStorage.setItem(FILTER_STORAGE_KEY, JSON.stringify({ + filter, + timestamp: Date.now() + })); + } catch { + // Ignore storage errors + } +}; + /** * Credentials list page. */ @@ -30,6 +72,8 @@ const CredentialsList: React.FC = () => { const { setHeaderButtons } = useHeaderButtons(); const [credentials, setCredentials] = useState([]); const [searchTerm, setSearchTerm] = useState(''); + const [filterType, setFilterType] = useState(getStoredFilter()); + const [showFilterMenu, setShowFilterMenu] = useState(false); const { setIsInitialLoading } = useLoading(); /** @@ -132,7 +176,54 @@ const CredentialsList: React.FC = () => { refreshCredentials(); }, [dbContext?.sqliteClient, setIsLoading, setIsInitialLoading]); - const filteredCredentials = credentials.filter(credential => { + /** + * Get the title based on the active filter + */ + const getFilterTitle = () : string => { + switch (filterType) { + case 'passkeys': + return t('credentials.filters.passkeys'); + case 'aliases': + return t('credentials.filters.aliases'); + case 'userpass': + return t('credentials.filters.userpass'); + default: + return t('credentials.title'); + } + }; + + const filteredCredentials = credentials.filter((credential: Credential) => { + // First apply type filter + let passesTypeFilter = true; + + if (filterType === 'passkeys') { + passesTypeFilter = credential.HasPasskey === true; + } else if (filterType === 'aliases') { + // Check for non-empty alias fields (excluding email which is used everywhere) + passesTypeFilter = !!( + (credential.Alias?.FirstName && credential.Alias.FirstName.trim()) || + (credential.Alias?.LastName && credential.Alias.LastName.trim()) || + (credential.Alias?.NickName && credential.Alias.NickName.trim()) || + (credential.Alias?.Gender && credential.Alias.Gender.trim()) || + (credential.Alias?.BirthDate && credential.Alias.BirthDate.trim() && credential.Alias.BirthDate.trim() !== '0001-01-01 00:00:00') + ); + } else if (filterType === 'userpass') { + // Show only credentials that have username/password AND do NOT have alias fields AND do NOT have passkey + const hasAliasFields = !!( + (credential.Alias?.FirstName && credential.Alias.FirstName.trim()) || + (credential.Alias?.LastName && credential.Alias.LastName.trim()) || + (credential.Alias?.NickName && credential.Alias.NickName.trim()) || + (credential.Alias?.Gender && credential.Alias.Gender.trim()) || + (credential.Alias?.BirthDate && credential.Alias.BirthDate.trim() && credential.Alias.BirthDate.trim() !== '0001-01-01 00:00:00') + ); + passesTypeFilter = !!(credential.Username || credential.Password) && !credential.HasPasskey && !hasAliasFields; + } + + if (!passesTypeFilter) { + return false; + } + + // Then apply search filter const searchLower = searchTerm.toLowerCase(); /** @@ -164,7 +255,93 @@ const CredentialsList: React.FC = () => { return (
-

{t('credentials.title')}

+
+ + + {showFilterMenu && ( + <> +
setShowFilterMenu(false)} + /> +
+
+ + + + +
+
+ + )} +
diff --git a/apps/browser-extension/src/i18n/locales/en.json b/apps/browser-extension/src/i18n/locales/en.json index c999ca9d6..37bb1d682 100644 --- a/apps/browser-extension/src/i18n/locales/en.json +++ b/apps/browser-extension/src/i18n/locales/en.json @@ -214,6 +214,12 @@ "saveCredential": "Save credential", "deleteCredentialTitle": "Delete Credential", "deleteCredentialConfirm": "Are you sure you want to delete this credential? This action cannot be undone.", + "filters": { + "all": "All Credentials", + "passkeys": "Passkeys", + "aliases": "Aliases", + "userpass": "Username/Passwords" + }, "randomAlias": "Random Alias", "manual": "Manual", "service": "Service", diff --git a/apps/browser-extension/src/utils/dist/shared/models/vault/index.d.ts b/apps/browser-extension/src/utils/dist/shared/models/vault/index.d.ts index 6c9531382..c5e62f674 100644 --- a/apps/browser-extension/src/utils/dist/shared/models/vault/index.d.ts +++ b/apps/browser-extension/src/utils/dist/shared/models/vault/index.d.ts @@ -1,3 +1,5 @@ +import { B } from "vitest/dist/chunks/worker.d.CHGSOG0s.js"; + /** * Encryption key SQLite database type. */ @@ -64,7 +66,9 @@ type Credential = { Logo?: Uint8Array | number[]; Notes?: string; Alias: Alias; + HasPasskey: boolean; }; + /** * Alias SQLite database type. */