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';7import { notifyPasskeyToastClosed } from './lifecycle';89let currentDispose: (() => void) | null = null;10let currentRequestId: string | null = null;11let toastContainer: HTMLElement | null = null;12let shadowRoot: ShadowRoot | null = null;1314function ensureContainer(): ShadowRoot {15  if (!toastContainer) {16    toastContainer = document.createElement('div');17    toastContainer.id = 'peach-passkey-toast-root';18    shadowRoot = toastContainer.attachShadow({ mode: 'closed' });19    document.body.appendChild(toastContainer);2021    const style = document.createElement('style');22    style.textContent = styles;23    shadowRoot.appendChild(style);24  }25  return shadowRoot!;26}2728function applyAccentToShadow(): void {29  if (!shadowRoot) return;30  chrome.runtime.sendMessage({ type: 'GET_SETTINGS' }).then((resp: unknown) => {31    const name = (resp as { ok?: boolean; data?: { accentColor?: string } } | null)?.data?.accentColor;32    if (!name || !shadowRoot) return;33    const color = ACCENT_COLORS.find(c => c.name === name);34    if (!color) return;35    let el = shadowRoot.querySelector<HTMLStyleElement>('#peach-accent-var');36    if (!el) {37      el = document.createElement('style');38      el.id = 'peach-accent-var';39      shadowRoot.appendChild(el);40    }41    el.textContent = `* { --peach-accent: ${color.hex}; }`;42  }).catch(() => {});43}4445export function showPasskeyToast(payload: {46  site: string;47  displayName: string;48  username: string;49  requestId: string;50  toastToken: string;51}) {52  // Dismiss any existing toast, actively denying the request it holds.53  if (currentDispose) {54    dismissPasskeyToast(currentRequestId ?? undefined);55  }5657  const root = ensureContainer();58  applyAccentToShadow();59  const wrapper = document.createElement('div');60  root.appendChild(wrapper);6162  let dispose: (() => void) | null = null;6364  dispose = render(() => (65    <PasskeyToast66      {...payload}67      onSignIn={() => {68        chrome.runtime.sendMessage({69          type: 'PASSKEY_AUTH_RESPONSE',70          payload: { requestId: payload.requestId, approved: true },71        });72        cleanup();73      }}74      onDeny={() => {75        chrome.runtime.sendMessage({76          type: 'PASSKEY_AUTH_RESPONSE',77          payload: { requestId: payload.requestId, approved: false },78        });79        cleanup();80      }}81    />82  ), wrapper);8384  currentDispose = dispose;85  currentRequestId = payload.requestId;8687  function cleanup() {88    const ownedDispose = dispose;89    if (ownedDispose) {90      ownedDispose();91      dispose = null;92    }93    wrapper.remove();94    if (currentDispose === ownedDispose) {95      currentDispose = null;96      currentRequestId = null;97    }98    notifyPasskeyToastClosed(payload.toastToken);99  }100}101102export function dismissPasskeyToast(denyRequestId?: string) {103  if (denyRequestId) {104    chrome.runtime.sendMessage({105      type: 'PASSKEY_AUTH_RESPONSE',106      payload: { requestId: denyRequestId, approved: false },107    });108  }109  if (currentDispose) {110    currentDispose();111    currentDispose = null;112    currentRequestId = null;113  }114}115116chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {117  if (msg.type === 'SHOW_PASSKEY_TOAST') {118    showPasskeyToast(msg.payload);119    sendResponse({ ok: true });120  }121  if (msg.type === 'DISMISS_PASSKEY_TOAST') {122    dismissPasskeyToast();123    sendResponse({ ok: true });124  }125});