PasskeyEntry.tsx

6.6 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-header" style="position:relative;z-index:1;">59              <div class="peach-entry-header-name">60                <h1 class={`peach-entry-name ${titleSizeClass(entry().title)}`} aria-label={entry().title}>61                  <span class="peach-entry-name-text">{entry().title}</span>62                </h1>63              </div>64              <Show65                when={deleteConfirmPending()}66                fallback={67                  <button68                    class="peach-entry-trash-btn"69                    onClick={() => setDeleteConfirmPending(true)}70                    aria-label="Remove passkey"71                    title="Remove passkey"72                  >73                    <svg width="12" height="13" viewBox="0 0 12 13" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">74                      <path d="M1 3h10M4 3V2h4v1M2 3l.7 8.5a1 1 0 0 0 1 .9h4.6a1 1 0 0 0 1-.9L10 3" />75                      <path d="M4.5 6l.3 3.5M7.5 6l-.3 3.5" />76                    </svg>77                  </button>78                }79              >80                <div class="peach-entry-trash-confirm">81                  <span class="peach-entry-trash-confirm-label">remove?</span>82                  <button83                    class="peach-btn peach-btn--sm peach-btn--danger"84                    style="font-size:10px;padding:2px 8px;min-height:22px;"85                    onClick={() => {86                      setDeleteConfirmPending(false);87                      void props.onDelete();88                    }}89                  >90                    yes91                  </button>92                  <button93                    class="peach-btn peach-btn--ghost peach-btn--sm"94                    style="font-size:10px;padding:2px 8px;min-height:22px;"95                    onClick={() => setDeleteConfirmPending(false)}96                  >97                    no98                  </button>99                </div>100              </Show>101            </div>102103            <div class="peach-domain" style="position:relative;z-index:1;">104              <div class="peach-domain-row">105                <span class="peach-domain-value">{entry().rpId}</span>106                <span class="peach-passkey-badge">passkey</span>107              </div>108            </div>109110            <section class="peach-fields" style="position:relative;z-index:1;">111              <div class="peach-field-card">112                <span class="peach-label">username</span>113                <div class="peach-field-value-row">114                  <span class="peach-field-value">{entry().userName}</span>115                </div>116              </div>117118              <div class="peach-field-card">119                <span class="peach-label">display name</span>120                <div class="peach-field-value-row">121                  <span class="peach-field-value">{entry().userDisplayName}</span>122                </div>123              </div>124125              <div class="peach-field-card">126                <span class="peach-label">algorithm</span>127                <div class="peach-field-value-row">128                  <span class="peach-field-value">{algorithmLabel(entry().algorithm)}</span>129                </div>130              </div>131132              <div class="peach-field-card">133                <span class="peach-label">activity</span>134                <div class="peach-field-value-row peach-passkey-activity">135                  <span class="peach-field-value">{formatUsedCount(entry().signCount)}</span>136                  <span class="peach-passkey-meta">created {formatDate(entry().createdAt)}</span>137                  <span class="peach-passkey-meta">last used {formatDate(entry().lastUsedAt)}</span>138                </div>139              </div>140            </section>141142            <div class="peach-passkey-actions">143              <Show when={crossBrowserMessage(entry().browser)}>144                {(msg) => (145                  <div class="peach-info-notice peach-info-notice--warn">146                    {msg()}147                  </div>148                )}149              </Show>150              <div class="peach-info-notice">151                {entry().browser === "firefox"152                  ? "This passkey is held by your platform authenticator. Peach stores only the metadata."153                  : "The private key is stored encrypted in your Peach vault and is never shown in the UI."}154              </div>155            </div>156          </>157        )}158      </Show>159    </div>160  );161}