From 765341c7aff7b2f70accbea94b4db1a424ed6f02 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Fri, 12 Dec 2025 19:05:25 +0100 Subject: [PATCH] Tweak passkey create and auth flow, replace literal item type with references (#1404) --- .../Credentials/Details/PasskeyBlock.tsx | 109 ++++++++++ .../Credentials/Details/PasskeyEditor.tsx | 185 ++++++++++++++++ .../components/Credentials/Details/index.tsx | 6 +- .../popup/components/Items/ItemIcon.tsx | 6 +- .../components/Items/ItemTypeSelector.tsx | 11 +- .../popup/pages/credentials/ItemAddEdit.tsx | 28 ++- .../popup/pages/credentials/ItemDetails.tsx | 11 +- .../popup/pages/passkeys/PasskeyCreate.tsx | 201 ++++++++++++++++-- .../src/i18n/locales/en.json | 7 +- .../src/utils/SqliteClient.ts | 6 +- .../utils/dist/core/models/vault/index.d.ts | 21 +- .../src/utils/dist/core/models/vault/index.js | 8 +- .../utils/dist/core/models/vault/index.d.ts | 21 +- .../utils/dist/core/models/vault/index.js | 8 +- core/models/src/vault/Item.ts | 24 +-- 15 files changed, 587 insertions(+), 65 deletions(-) create mode 100644 apps/browser-extension/src/entrypoints/popup/components/Credentials/Details/PasskeyBlock.tsx create mode 100644 apps/browser-extension/src/entrypoints/popup/components/Credentials/Details/PasskeyEditor.tsx diff --git a/apps/browser-extension/src/entrypoints/popup/components/Credentials/Details/PasskeyBlock.tsx b/apps/browser-extension/src/entrypoints/popup/components/Credentials/Details/PasskeyBlock.tsx new file mode 100644 index 000000000..b5c8844a8 --- /dev/null +++ b/apps/browser-extension/src/entrypoints/popup/components/Credentials/Details/PasskeyBlock.tsx @@ -0,0 +1,109 @@ +import React, { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; + +import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner'; +import { useDb } from '@/entrypoints/popup/context/DbContext'; + +import type { Passkey } from '@/utils/dist/core/models/vault'; + +type PasskeyBlockProps = { + itemId: string; +} + +/** + * Passkey icon component. + */ +const PasskeyIcon: React.FC<{ className?: string }> = ({ className = "w-5 h-5" }) => ( + + + +); + +/** + * Display passkey information for an item in view mode. + */ +const PasskeyBlock: React.FC = ({ itemId }) => { + const { t } = useTranslation(); + const dbContext = useDb(); + const [passkeys, setPasskeys] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (!dbContext?.sqliteClient || !itemId) { + setLoading(false); + return; + } + + try { + const itemPasskeys = dbContext.sqliteClient.getPasskeysByItemId(itemId); + setPasskeys(itemPasskeys); + } catch (err) { + console.error('Error loading passkeys:', err); + } finally { + setLoading(false); + } + }, [dbContext?.sqliteClient, itemId]); + + if (loading) { + return ( +
+ +
+ ); + } + + if (passkeys.length === 0) { + return null; + } + + return ( +
+

+ {t('passkeys.passkey')} +

+ {passkeys.map((passkey) => ( +
+
+ +
+
+ + {passkey.DisplayName || t('passkeys.passkey')} + +
+
+ {passkey.RpId && ( +
+ + {t('passkeys.site')}:{' '} + + + {passkey.RpId} + +
+ )} +
+

+ {t('passkeys.helpText')} +

+
+
+
+ ))} +
+ ); +}; + +export default PasskeyBlock; +export { PasskeyIcon }; diff --git a/apps/browser-extension/src/entrypoints/popup/components/Credentials/Details/PasskeyEditor.tsx b/apps/browser-extension/src/entrypoints/popup/components/Credentials/Details/PasskeyEditor.tsx new file mode 100644 index 000000000..b19ad08e2 --- /dev/null +++ b/apps/browser-extension/src/entrypoints/popup/components/Credentials/Details/PasskeyEditor.tsx @@ -0,0 +1,185 @@ +import React, { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; + +import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner'; +import { useDb } from '@/entrypoints/popup/context/DbContext'; + +import type { Passkey } from '@/utils/dist/core/models/vault'; + +import { PasskeyIcon } from './PasskeyBlock'; + +type PasskeyEditorProps = { + itemId: string; + passkeyIdsMarkedForDeletion: string[]; + onPasskeyMarkedForDeletion: (passkeyIds: string[]) => void; +} + +/** + * Edit passkey information for an item (supports marking for deletion). + * Passkeys cannot be manually created or edited - only deleted. + */ +const PasskeyEditor: React.FC = ({ + itemId, + passkeyIdsMarkedForDeletion, + onPasskeyMarkedForDeletion +}) => { + const { t } = useTranslation(); + const dbContext = useDb(); + const [passkeys, setPasskeys] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (!dbContext?.sqliteClient || !itemId) { + setLoading(false); + return; + } + + try { + const itemPasskeys = dbContext.sqliteClient.getPasskeysByItemId(itemId); + setPasskeys(itemPasskeys); + } catch (err) { + console.error('Error loading passkeys:', err); + } finally { + setLoading(false); + } + }, [dbContext?.sqliteClient, itemId]); + + /** + * Mark a passkey for deletion. + */ + const handleMarkForDeletion = (passkeyId: string): void => { + if (!passkeyIdsMarkedForDeletion.includes(passkeyId)) { + onPasskeyMarkedForDeletion([...passkeyIdsMarkedForDeletion, passkeyId]); + } + }; + + /** + * Undo marking a passkey for deletion. + */ + const handleUndoDeletion = (passkeyId: string): void => { + onPasskeyMarkedForDeletion(passkeyIdsMarkedForDeletion.filter(id => id !== passkeyId)); + }; + + if (loading) { + return ( +
+ +
+ ); + } + + if (passkeys.length === 0) { + return null; + } + + return ( +
+

+ {t('passkeys.passkey')} +

+
+ {passkeys.map((passkey) => { + const isMarkedForDeletion = passkeyIdsMarkedForDeletion.includes(passkey.Id); + + if (isMarkedForDeletion) { + return ( +
+
+ +
+
+ + {t('passkeys.passkeyMarkedForDeletion')} + + +
+

+ {t('passkeys.passkeyWillBeDeleted')} +

+
+
+
+ ); + } + + return ( +
+
+ +
+
+ + {passkey.DisplayName || t('passkeys.passkey')} + + +
+
+ {passkey.RpId && ( +
+ + {t('passkeys.site')}:{' '} + + + {passkey.RpId} + +
+ )} +
+

+ {t('passkeys.helpText')} +

+
+
+
+ ); + })} +
+
+ ); +}; + +export default PasskeyEditor; diff --git a/apps/browser-extension/src/entrypoints/popup/components/Credentials/Details/index.tsx b/apps/browser-extension/src/entrypoints/popup/components/Credentials/Details/index.tsx index 64fe860b8..54a55417c 100644 --- a/apps/browser-extension/src/entrypoints/popup/components/Credentials/Details/index.tsx +++ b/apps/browser-extension/src/entrypoints/popup/components/Credentials/Details/index.tsx @@ -5,6 +5,8 @@ import FieldBlock from './FieldBlock'; import HeaderBlock from './HeaderBlock'; import LoginCredentialsBlock from './LoginCredentialsBlock'; import NotesBlock from './NotesBlock'; +import PasskeyBlock from './PasskeyBlock'; +import PasskeyEditor from './PasskeyEditor'; import TotpBlock from './TotpBlock'; export { @@ -15,5 +17,7 @@ export { AliasBlock, NotesBlock, AttachmentBlock, - FieldBlock + FieldBlock, + PasskeyBlock, + PasskeyEditor }; \ No newline at end of file diff --git a/apps/browser-extension/src/entrypoints/popup/components/Items/ItemIcon.tsx b/apps/browser-extension/src/entrypoints/popup/components/Items/ItemIcon.tsx index bb6222df6..9409db24d 100644 --- a/apps/browser-extension/src/entrypoints/popup/components/Items/ItemIcon.tsx +++ b/apps/browser-extension/src/entrypoints/popup/components/Items/ItemIcon.tsx @@ -1,7 +1,7 @@ import React from 'react'; import type { Item } from '@/utils/dist/core/models/vault'; -import { FieldKey } from '@/utils/dist/core/models/vault'; +import { FieldKey, ItemTypes } from '@/utils/dist/core/models/vault'; import SqliteClient from '@/utils/SqliteClient'; type ItemIconProps = { @@ -232,12 +232,12 @@ const getCardIcon = (brand: CardBrand): React.FC<{ className?: string }> => { */ const ItemIcon: React.FC = ({ item, className = 'w-8 h-8' }) => { // For Note type, always show note icon - if (item.ItemType === 'Note') { + if (item.ItemType === ItemTypes.Note) { return ; } // For CreditCard type, detect card brand and show appropriate icon - if (item.ItemType === 'CreditCard') { + if (item.ItemType === ItemTypes.CreditCard) { const cardNumberField = item.Fields?.find(f => f.FieldKey === FieldKey.CardNumber); const cardNumber = cardNumberField?.Value ? (Array.isArray(cardNumberField.Value) ? cardNumberField.Value[0] : cardNumberField.Value) diff --git a/apps/browser-extension/src/entrypoints/popup/components/Items/ItemTypeSelector.tsx b/apps/browser-extension/src/entrypoints/popup/components/Items/ItemTypeSelector.tsx index e9d4a277b..ba4072ec1 100644 --- a/apps/browser-extension/src/entrypoints/popup/components/Items/ItemTypeSelector.tsx +++ b/apps/browser-extension/src/entrypoints/popup/components/Items/ItemTypeSelector.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import type { ItemType } from '@/utils/dist/core/models/vault'; +import { ItemTypes } from '@/utils/dist/core/models/vault'; /** * Item type option configuration. @@ -17,7 +18,7 @@ type ItemTypeOption = { */ const ITEM_TYPE_OPTIONS: ItemTypeOption[] = [ { - type: 'Login', + type: ItemTypes.Login, titleKey: 'itemTypes.login.title', iconSvg: ( @@ -26,7 +27,7 @@ const ITEM_TYPE_OPTIONS: ItemTypeOption[] = [ ) }, { - type: 'Alias', + type: ItemTypes.Alias, titleKey: 'itemTypes.alias.title', iconSvg: ( @@ -35,7 +36,7 @@ const ITEM_TYPE_OPTIONS: ItemTypeOption[] = [ ) }, { - type: 'CreditCard', + type: ItemTypes.CreditCard, titleKey: 'itemTypes.creditCard.title', iconSvg: ( @@ -44,7 +45,7 @@ const ITEM_TYPE_OPTIONS: ItemTypeOption[] = [ ) }, { - type: 'Note', + type: ItemTypes.Note, titleKey: 'itemTypes.note.title', iconSvg: ( @@ -105,7 +106,7 @@ const ItemTypeSelector: React.FC = ({ {/* Regenerate alias button - icon only for flexibility */} - {selectedType === 'Alias' && !isEditMode && onRegenerateAlias && ( + {selectedType === ItemTypes.Alias && !isEditMode && onRegenerateAlias && (
)} + {/* Step 1b: Show matching credentials to attach passkey to (when no existing passkeys) */} + {!showCreateForm && existingPasskeys.length === 0 && matchingCredentials.length > 0 && ( +
+ + + + +
+
+
+
+
+ + {t('common.or')} + +
+
+ +
+ +

+ {t('passkeys.create.selectExistingLoginDescription')} +

+
+ {matchingCredentials.map((credential) => ( + + ))} +
+
+ + +
+ )} + {/* Step 2: Show create form with display name */} {showCreateForm && (
@@ -581,29 +737,44 @@ const PasskeyCreate: React.FC = () => { )} - + {selectedCredentialToAttach && ( + + {t('passkeys.create.attachingToCredential', { + serviceName: matchingCredentials.find(c => c.Id === selectedCredentialToAttach)?.ServiceName || '' + })} + + )} + + {!selectedCredentialToAttach && ( + + )}
- {existingPasskeys.length > 0 ? ( + {(existingPasskeys.length > 0 || matchingCredentials.length > 0) ? (