inject.tsx

3.6 KB · raw

1import { render } from 'solid-js/web';2import { PasskeyToast } from './PasskeyToast';3// ?inline imports the CSS as a string so we can inject it into the shadow root4// rather than letting Vite/crx inject it into the page document head.5import styles from './passkey-toast.css?inline';6import { ACCENT_COLORS } from '../../shared/types';78let currentDispose: (() => void) | null = null;9let currentRequestId: string | null = null;10let toastContainer: HTMLElement | null = null;11let shadowRoot: ShadowRoot | null = null;1213function ensureContainer(): ShadowRoot {14  if (!toastContainer) {15    toastContainer = document.createElement('div');16    toastContainer.id = 'peach-passkey-toast-root';17    shadowRoot = toastContainer.attachShadow({ mode: 'closed' });18    document.body.appendChild(toastContainer);1920    const style = document.createElement('style');21    style.textContent = styles;22    shadowRoot.appendChild(style);23  }24  return shadowRoot!;25}2627function applyAccentToShadow(): void {28  if (!shadowRoot) return;29  chrome.runtime.sendMessage({ type: 'GET_SETTINGS' }).then((resp: unknown) => {30    const name = (resp as { ok?: boolean; data?: { accentColor?: string } } | null)?.data?.accentColor;31    if (!name || !shadowRoot) return;32    const color = ACCENT_COLORS.find(c => c.name === name);33    if (!color) return;34    let el = shadowRoot.querySelector<HTMLStyleElement>('#peach-accent-var');35    if (!el) {36      el = document.createElement('style');37      el.id = 'peach-accent-var';38      shadowRoot.appendChild(el);39    }40    el.textContent = `* { --peach-accent: ${color.hex}; }`;41  }).catch(() => {});42}4344export function showPasskeyToast(payload: {45  site: string;46  displayName: string;47  username: string;48  requestId: string;49  toastToken: string;50}) {51  // Dismiss any existing toast, actively denying the request it holds.52  if (currentDispose) {53    dismissPasskeyToast(currentRequestId ?? undefined);54  }5556  const root = ensureContainer();57  applyAccentToShadow();58  const wrapper = document.createElement('div');59  root.appendChild(wrapper);6061  let dispose: (() => void) | null = null;6263  dispose = render(() => (64    <PasskeyToast65      {...payload}66      onSignIn={() => {67        chrome.runtime.sendMessage({68          type: 'PASSKEY_AUTH_RESPONSE',69          payload: { requestId: payload.requestId, approved: true },70        });71        cleanup();72      }}73      onDeny={() => {74        chrome.runtime.sendMessage({75          type: 'PASSKEY_AUTH_RESPONSE',76          payload: { requestId: payload.requestId, approved: false },77        });78        cleanup();79      }}80    />81  ), wrapper);8283  currentDispose = dispose;84  currentRequestId = payload.requestId;8586  function cleanup() {87    if (dispose) {88      dispose();89      dispose = null;90    }91    wrapper.remove();92    currentDispose = null;93    currentRequestId = null;94    // Tell index.ts the toast is gone so it can re-enable autofill.95    window.postMessage({ source: 'peach-passkey-toast', type: 'toast-closed', token: payload.toastToken }, '*');96  }97}9899export function dismissPasskeyToast(denyRequestId?: string) {100  if (denyRequestId) {101    chrome.runtime.sendMessage({102      type: 'PASSKEY_AUTH_RESPONSE',103      payload: { requestId: denyRequestId, approved: false },104    });105  }106  if (currentDispose) {107    currentDispose();108    currentDispose = null;109    currentRequestId = null;110  }111}112113chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {114  if (msg.type === 'SHOW_PASSKEY_TOAST') {115    showPasskeyToast(msg.payload);116    sendResponse({ ok: true });117  }118  if (msg.type === 'DISMISS_PASSKEY_TOAST') {119    dismissPasskeyToast();120    sendResponse({ ok: true });121  }122});