PasskeyEntry.tsx

6.0 KB · raw

1import { createSignal, Show } from "solid-js";23import type { PasskeyEntry } from "../../shared/types";4import { detectBrowser } from "../../shared/browser";56interface PasskeyEntryProps {7  entry: PasskeyEntry | null;8  onDelete: () => void | Promise<void>;9}1011function formatDate(timestamp?: number): string {12  if (!timestamp) return "Never";13  return new Date(timestamp).toLocaleDateString();14}1516function formatUsedCount(count?: number): string {17  const used = count ?? 0;18  return `used ${used} time${used === 1 ? "" : "s"}`;19}2021function algorithmLabel(algorithm: -7 | -8): string {22  return algorithm === -8 ? "EdDSA (Ed25519)" : "ES256 (P-256)";23}2425function titleSizeClass(title: string): string {26  if (title.length >= 28) return "peach-entry-name--xxl";27  if (title.length >= 20) return "peach-entry-name--xl";28  if (title.length >= 14) return "peach-entry-name--lg";29  return "";30}3132export function PasskeyEntryView(props: PasskeyEntryProps) {33  const [deleteConfirmPending, setDeleteConfirmPending] = createSignal(false);34  const currentBrowser = detectBrowser();3536  function crossBrowserMessage(entryBrowser: "chrome" | "firefox" | undefined): string | null {37    if (!entryBrowser) return null;38    if (currentBrowser === "unknown") return null;39    if (entryBrowser === currentBrowser) return null;40    const from = entryBrowser === "chrome" ? "Chrome" : "Firefox";41    const to = currentBrowser === "chrome" ? "Chrome" : "Firefox";42    return `This passkey was created on ${from}. To use it on ${to}, re-register it on this device.`;43  }4445  return (46    <div class="peach-stage" style="position:relative;">47      <Show when={props.entry} fallback={<div class="peach-list-empty"><span>no passkey selected</span></div>}>48        {(entry) => (49          <>50            <div51              class="peach-ghost"52              aria-hidden="true"53              style="position:absolute;top:30px;left:12px;right:0;font-family:'Cormorant Garamond',Georgia,serif;font-size:150px;font-style:italic;font-weight:400;color:var(--t0);opacity:0.03;pointer-events:none;line-height:1;overflow:hidden;white-space:nowrap;z-index:0;"54            >55              {entry().title}56            </div>5758            <div class="peach-entry-name-wrap" style="position:relative;z-index:1;">59              <h1 class={`peach-entry-name ${titleSizeClass(entry().title)}`} aria-label={entry().title}>60                <span class="peach-entry-name-text">{entry().title}</span>61              </h1>62            </div>6364            <div class="peach-domain" style="position:relative;z-index:1;">65              <div class="peach-domain-row">66                <span class="peach-domain-value">{entry().rpId}</span>67                <span class="peach-passkey-badge">passkey</span>68              </div>69            </div>7071            <section class="peach-fields" style="position:relative;z-index:1;">72              <div class="peach-field-card">73                <span class="peach-label">username</span>74                <div class="peach-field-value-row">75                  <span class="peach-field-value">{entry().userName}</span>76                </div>77              </div>7879              <div class="peach-field-card">80                <span class="peach-label">display name</span>81                <div class="peach-field-value-row">82                  <span class="peach-field-value">{entry().userDisplayName}</span>83                </div>84              </div>8586              <div class="peach-field-card">87                <span class="peach-label">algorithm</span>88                <div class="peach-field-value-row">89                  <span class="peach-field-value">{algorithmLabel(entry().algorithm)}</span>90                </div>91              </div>9293              <div class="peach-field-card">94                <span class="peach-label">activity</span>95                <div class="peach-field-value-row peach-passkey-activity">96                  <span class="peach-field-value">{formatUsedCount(entry().signCount)}</span>97                  <span class="peach-passkey-meta">created {formatDate(entry().createdAt)}</span>98                  <span class="peach-passkey-meta">last used {formatDate(entry().lastUsedAt)}</span>99                </div>100              </div>101            </section>102103            <div class="peach-passkey-actions">104              <Show when={crossBrowserMessage(entry().browser)}>105                {(msg) => (106                  <div class="peach-info-notice peach-info-notice--warn">107                    {msg()}108                  </div>109                )}110              </Show>111              <div class="peach-info-notice">112                {entry().browser === "firefox"113                  ? "This passkey is held by your platform authenticator. Peach stores only the metadata."114                  : "The private key is stored encrypted in your Peach vault and is never shown in the UI."}115              </div>116              <Show117                when={deleteConfirmPending()}118                fallback={119                  <button120                    class="peach-btn peach-btn--ghost peach-btn--sm peach-passkey-delete-btn"121                    onClick={() => setDeleteConfirmPending(true)}122                  >123                    remove passkey124                  </button>125                }126              >127                <div class="peach-passkey-delete-confirm">128                  <span class="peach-passkey-delete-confirm-copy">Remove this passkey?</span>129                  <button130                    class="peach-btn peach-btn--ghost peach-btn--sm"131                    onClick={() => setDeleteConfirmPending(false)}132                  >133                    cancel134                  </button>135                  <button136                    class="peach-btn peach-btn--ghost peach-btn--sm peach-passkey-delete-btn--confirm"137                    onClick={() => {138                      setDeleteConfirmPending(false);139                      void props.onDelete();140                    }}141                  >142                    remove143                  </button>144                </div>145              </Show>146            </div>147          </>148        )}149      </Show>150    </div>151  );152}