mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-09-20 18:09:05 +02:00
feat: include passkey icons based on AAGUID (#1756)
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
|
||||
let {
|
||||
icon,
|
||||
providerIconUrl,
|
||||
onRename,
|
||||
onDelete,
|
||||
showRenameAction = true,
|
||||
@@ -14,18 +15,37 @@
|
||||
description
|
||||
}: {
|
||||
icon: LucideIcon;
|
||||
providerIconUrl?: string;
|
||||
onRename?: () => void;
|
||||
onDelete: () => void;
|
||||
showRenameAction?: boolean;
|
||||
description?: string;
|
||||
label?: string;
|
||||
} = $props();
|
||||
|
||||
// Falls back to the generic icon when the authenticator icon cannot be loaded
|
||||
let iconFailed = $state(false);
|
||||
|
||||
// The row is reused across passkeys and themes, so a previous failure must not hide an icon that is now a different URL
|
||||
$effect(() => {
|
||||
void providerIconUrl;
|
||||
iconFailed = false;
|
||||
});
|
||||
|
||||
const showProviderIcon = $derived(!!providerIconUrl && !iconFailed);
|
||||
</script>
|
||||
|
||||
<Item.Root variant="transparent" class="hover:bg-muted transition-colors py-3 px-0 sm:px-4">
|
||||
<Item.Media class="bg-primary/10 text-primary rounded-full p-3">
|
||||
{#if icon}{@const Icon = icon}
|
||||
<Icon class="size-5" />
|
||||
<Item.Media class="bg-muted text-muted-foreground size-11 rounded-xl">
|
||||
{#if showProviderIcon}
|
||||
<img
|
||||
src={providerIconUrl}
|
||||
alt=""
|
||||
class="size-7 object-contain"
|
||||
onerror={() => (iconFailed = true)}
|
||||
/>
|
||||
{:else if icon}{@const Icon = icon}
|
||||
<Icon class="size-6" />
|
||||
{/if}
|
||||
</Item.Media>
|
||||
<Item.Content class="gap-0.5">
|
||||
|
||||
@@ -2,4 +2,6 @@ export type Passkey = {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: string;
|
||||
aaguid: string;
|
||||
hasIcon: boolean;
|
||||
};
|
||||
|
||||
@@ -75,6 +75,17 @@ export const cachedOidcClientLogo: CachableImage = {
|
||||
}
|
||||
};
|
||||
|
||||
// Builds the URL of an authenticator icon
|
||||
// Unlike the images above these are static assets that only change with a new release, so they skip the cache busting helper and rely on the backend's cache headers
|
||||
export function authenticatorIconUrl(aaguid: string, light = true) {
|
||||
const url = new URL(
|
||||
`/api/webauthn/authenticator-icons/${encodeURIComponent(aaguid)}`,
|
||||
window.location.origin
|
||||
);
|
||||
if (!light) url.searchParams.set('light', 'false');
|
||||
return url.pathname + (url.search ? `?${url.searchParams.toString()}` : '');
|
||||
}
|
||||
|
||||
function getCachedImageUrl(url: URL) {
|
||||
const baseKey = normalizeUrlForKey(url);
|
||||
const skipCacheUntil = getSkipCacheUntil(baseKey);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { untrack } from 'svelte';
|
||||
import { fromStore, type Readable } from 'svelte/store';
|
||||
|
||||
/**
|
||||
* Registers a section of a page (e.g. a card) with the unsaved-changes bar.
|
||||
* Registers a section of a page (e.g. a card) with the unsaved-changes bar.
|
||||
* The section is unregistered when the calling component is destroyed.
|
||||
*/
|
||||
export function trackUnsavedSection(
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import WebauthnService from '$lib/services/webauthn-service';
|
||||
import type { Passkey } from '$lib/types/passkey.type';
|
||||
import { authenticatorIconUrl } from '$lib/utils/cached-image-util';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
import { LucideKeyRound } from '@lucide/svelte';
|
||||
import { mode } from 'mode-watcher';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import RenamePasskeyModal from './rename-passkey-modal.svelte';
|
||||
|
||||
@@ -16,6 +18,8 @@
|
||||
|
||||
let passkeyToRename: Passkey | null = $state(null);
|
||||
|
||||
const isLightMode = $derived(mode.current === 'light');
|
||||
|
||||
async function deletePasskey(passkey: Passkey) {
|
||||
openConfirmDialog({
|
||||
title: m.delete_passkey_name({ passkeyName: passkey.name }),
|
||||
@@ -43,6 +47,9 @@
|
||||
label={passkey.name}
|
||||
description={m.added_on() + ' ' + new Date(passkey.createdAt).toLocaleDateString()}
|
||||
icon={LucideKeyRound}
|
||||
providerIconUrl={passkey.hasIcon
|
||||
? authenticatorIconUrl(passkey.aaguid, isLightMode)
|
||||
: undefined}
|
||||
onRename={() => (passkeyToRename = passkey)}
|
||||
onDelete={() => deletePasskey(passkey)}
|
||||
/>
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import UserService from '$lib/services/user-service';
|
||||
import type { Passkey } from '$lib/types/passkey.type';
|
||||
import { authenticatorIconUrl } from '$lib/utils/cached-image-util';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
import { LucideKeyRound } from '@lucide/svelte';
|
||||
import { mode } from 'mode-watcher';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
let {
|
||||
@@ -19,6 +21,8 @@
|
||||
|
||||
const userService = new UserService();
|
||||
|
||||
const isLightMode = $derived(mode.current === 'light');
|
||||
|
||||
async function refreshPasskeys() {
|
||||
passkeys = await userService.listUserPasskeys(userId);
|
||||
}
|
||||
@@ -50,6 +54,9 @@
|
||||
label={passkey.name}
|
||||
description={m.added_on() + ' ' + new Date(passkey.createdAt).toLocaleDateString()}
|
||||
icon={LucideKeyRound}
|
||||
providerIconUrl={passkey.hasIcon
|
||||
? authenticatorIconUrl(passkey.aaguid, isLightMode)
|
||||
: undefined}
|
||||
showRenameAction={false}
|
||||
onDelete={() => deletePasskey(passkey)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user