import { render } from 'solid-js/web'; import { PasskeyToast } from './PasskeyToast'; // ?inline imports the CSS as a string so we can inject it into the shadow root // rather than letting Vite/crx inject it into the page document head. import styles from './passkey-toast.css?inline'; import { ACCENT_COLORS } from '../../shared/types'; let currentDispose: (() => void) | null = null; let currentRequestId: string | null = null; let toastContainer: HTMLElement | null = null; let shadowRoot: ShadowRoot | null = null; function ensureContainer(): ShadowRoot { if (!toastContainer) { toastContainer = document.createElement('div'); toastContainer.id = 'peach-passkey-toast-root'; shadowRoot = toastContainer.attachShadow({ mode: 'closed' }); document.body.appendChild(toastContainer); const style = document.createElement('style'); style.textContent = styles; shadowRoot.appendChild(style); } return shadowRoot!; } function applyAccentToShadow(): void { if (!shadowRoot) return; chrome.runtime.sendMessage({ type: 'GET_SETTINGS' }).then((resp: unknown) => { const name = (resp as { ok?: boolean; data?: { accentColor?: string } } | null)?.data?.accentColor; if (!name || !shadowRoot) return; const color = ACCENT_COLORS.find(c => c.name === name); if (!color) return; let el = shadowRoot.querySelector('#peach-accent-var'); if (!el) { el = document.createElement('style'); el.id = 'peach-accent-var'; shadowRoot.appendChild(el); } el.textContent = `* { --peach-accent: ${color.hex}; }`; }).catch(() => {}); } export function showPasskeyToast(payload: { site: string; displayName: string; username: string; requestId: string; }) { // Dismiss any existing toast, actively denying the request it holds. if (currentDispose) { dismissPasskeyToast(currentRequestId ?? undefined); } const root = ensureContainer(); applyAccentToShadow(); const wrapper = document.createElement('div'); root.appendChild(wrapper); let dispose: (() => void) | null = null; dispose = render(() => ( { chrome.runtime.sendMessage({ type: 'PASSKEY_AUTH_RESPONSE', payload: { requestId: payload.requestId, approved: true }, }); cleanup(); }} onDeny={() => { chrome.runtime.sendMessage({ type: 'PASSKEY_AUTH_RESPONSE', payload: { requestId: payload.requestId, approved: false }, }); cleanup(); }} /> ), wrapper); currentDispose = dispose; currentRequestId = payload.requestId; function cleanup() { if (dispose) { dispose(); dispose = null; } wrapper.remove(); currentDispose = null; currentRequestId = null; // Tell index.ts the toast is gone so it can re-enable autofill. window.postMessage({ source: 'peach-passkey-toast', type: 'toast-closed' }, '*'); } } export function dismissPasskeyToast(denyRequestId?: string) { if (denyRequestId) { chrome.runtime.sendMessage({ type: 'PASSKEY_AUTH_RESPONSE', payload: { requestId: denyRequestId, approved: false }, }); } if (currentDispose) { currentDispose(); currentDispose = null; currentRequestId = null; } } chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { if (msg.type === 'SHOW_PASSKEY_TOAST') { showPasskeyToast(msg.payload); sendResponse({ ok: true }); } if (msg.type === 'DISMISS_PASSKEY_TOAST') { dismissPasskeyToast(); sendResponse({ ok: true }); } });